Skip to content

perf(transformer): pre-size JSX props vec to attribute count#23466

Merged
camc314 merged 2 commits into
oxc-project:mainfrom
hyf0:perf/transformer-jsx-properties-presize
Jun 16, 2026
Merged

perf(transformer): pre-size JSX props vec to attribute count#23466
camc314 merged 2 commits into
oxc-project:mainfrom
hyf0:perf/transformer-jsx-properties-presize

Conversation

@hyf0

@hyf0 hyf0 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Memory reduction: fewer arena reallocations on JSX

transform_jsx built the React.createElement / jsx props object in a zero-capacity arena vec
(ctx.ast.vec()), regrowing it (4 → 8 → 16 …) as it pushed one property per attribute. The attribute
count is already known, so pre-sizing the vec to it removes those regrowths — mirroring arguments,
pre-sized a few lines above, and the same pattern as #9915.

Measured with cargo allocs (TestFiles::complicated). Arena reallocations drop on every JSX file;
nothing else changes
(system allocs and the arena-alloc count are essentially flat):

file arena reallocs: before → after reduction
App.tsx 77 → 25 −52 (−68%)
kitchen-sink.tsx 517 → 304 −213 (−41%)
RadixUIAdoptionSection.jsx 24 → 13 −11 (−46%)

Each eliminated realloc is a buffer grow + memcpy of the existing elements, so this is real
work removed on the JSX hot path, not just bytes.

Raw allocs_transformer.snap diff (updated in this PR)
-App.tsx                    | 415.34 kB      ||             25 |              2 ||            719 |             77
+App.tsx                    | 415.34 kB      ||             25 |              2 ||            719 |             25
-RadixUIAdoptionSection.jsx | 2.52 kB        ||             21 |              2 ||            259 |             24
+RadixUIAdoptionSection.jsx | 2.52 kB        ||             21 |              2 ||            259 |             13
-kitchen-sink.tsx           | 732.90 kB      ||            184 |              3 ||           6689 |            517
+kitchen-sink.tsx           | 732.90 kB      ||            184 |              3 ||           6690 |            304

(Non-JSX files unchanged. The lone +1 arena alloc on kitchen-sink is an eager 1-slot allocation for
a rare element whose attributes are all filtered out — negligible against −213 reallocs.)

Change

let mut properties =
    ctx.ast.vec_with_capacity(opening_element.as_ref().map_or(0, |e| e.attributes.len()));

Capacity-only: the vec's contents, push order, and emitted output are unchanged — only the initial
allocation differs. CodSpeed (instructions) may pick up the saved memcpys on JSX-heavy input, but this
is framed as a memory/allocation win.

Behavior-preserving

cargo test -p oxc_transformer passes. The change is a capacity hint and cannot alter the produced AST.


Prepared with AI assistance.

@codspeed-hq

codspeed-hq Bot commented Jun 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 52 untouched benchmarks
⏩ 19 skipped benchmarks1


Comparing hyf0:perf/transformer-jsx-properties-presize (ca608d6) with main (488b382)

Open in CodSpeed

Footnotes

  1. 19 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Dunqing Dunqing added 0-merge Merge with Graphite Merge Queue and removed 0-merge Merge with Graphite Merge Queue labels Jun 16, 2026
@hyf0 hyf0 marked this pull request as ready for review June 16, 2026 05:11
@hyf0 hyf0 requested a review from overlookmotel as a code owner June 16, 2026 05:11
Comment thread crates/oxc_transformer/src/jsx/jsx_impl.rs

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Comment thread crates/oxc_transformer/src/jsx/jsx_impl.rs
@camc314 camc314 added the A-transformer Area - Transformer / Transpiler label Jun 16, 2026
@camc314 camc314 merged commit 3fa8051 into oxc-project:main Jun 16, 2026
59 of 60 checks passed
camc314 pushed a commit that referenced this pull request Jun 16, 2026
…ering (#23516)

## What

Two zero-capacity arena `Vec`s in the TypeScript transformer were filled
in known-length loops and grew via reallocation. Pre-size them —
mirroring the JSX-props pre-size in #23466:

- **`transform_ts_enum_members`**: emits exactly `members.len() + 1`
statements (one per member + the final `return`) →
`vec_with_capacity(members.len() + 1)` (an **exact** capacity).
- **TS namespace `enter_program`**: rebuilds the program body; pre-size
to `program.body.len()` (an estimate — most statements pass through
1:1).

## Δ — fewer reallocations on TS fixtures

`allocs_transformer.snap`, reallocation count:

| fixture | before | after |
| --- | --- | --- |
| checker.ts | 64 | **5** |
| kitchen-sink.tsx | 304 | **280** |
| App.tsx | 25 | **17** |
| binder.ts | 6 | **0** |

(allocation **count** and **bytes** are unchanged — this removes the
intermediate growth reallocations.)

## Why it is behavior-preserving

Capacity-only: `vec_with_capacity` differs from `vec` solely in initial
capacity; element type, push order, and contents are identical, so the
produced AST is byte-identical. `oxc_transformer` tests pass.

Prepared with AI assistance.
Boshen added a commit that referenced this pull request Jun 18, 2026
### 💥 BREAKING CHANGES

- 7a76cd3 estree: [**BREAKING**] Make whether to include TS fields a
runtime option (#23574) (overlookmotel)
- e7b6b68 estree: [**BREAKING**] `ESTree` config use methods not consts
(#23573) (overlookmotel)

### 🚀 Features

- 556cc6d data_structures: Add `CodeBuffer::as_str` method (#23571)
(overlookmotel)
- 38c4b06 parser: Add friendly error for adjacent JSX elements (#23378)
(sapphi-red)
- 53509a8 minifier: Treeshake pure typed arrays and Set/Map array
literals (#23469) (Dunqing)
- 09762d9 minifier: Inline const value for read-only vars (#22593)
(Dunqing)

### 🐛 Bug Fixes

- 20375f9 react_compiler: Keep imports referenced only by a computed key
(#23586) (Boshen)
- 31bfd9b minifier: Keep Object introspection calls on a possible Proxy
(#23483) (Dunqing)
- 837a395 parser: Treat a line comment after ':' as leading, not
trailing (#23515) (Dunqing)
- e409fe0 minifier: Keep `new Map`/`WeakSet`/`WeakMap` with a string
argument (#23470) (Dunqing)
- ae02b4e ci/parser: Use `minimal` for vitest reporter (#23457)
(camc314)

### ⚡ Performance

- cf24329 mangler: Compile slot sort once instead of per CAPACITY
(#23577) (Boshen)
- 4058a6a parser: Reduce code bloat from verify_modifiers
monomorphization (#23576) (Boshen)
- 053b0c1 estree: Remove pointless `mem::take` (#23572) (overlookmotel)
- dfb52b6 transformer: Pre-size statement vecs in TS enum & namespace
lowering (#23516) (Yunfei He)
- 970e09a minifier: Compute template-literal inline checks in a single
pass (#23467) (Yunfei He)
- 3170c0e semantic,mangler,minifier: Fix `Semantic::stats` node count
and reuse stats in mangler builds (#23352) (Boshen)
- d1fa6e0 minifier: Evaluate ternary branches once in
minimize_conditional_expression (#23479) (Yunfei He)
- 3fa8051 transformer: Pre-size JSX props vec to attribute count
(#23466) (Yunfei He)
- 488b382 react_compiler: Borrow binding names in prefilter instead of
allocating (#23471) (Yunfei He)
- bcb3894 minifier: Incremental scoping refresh, delete
LiveUsageCollector (#23197) (Dunqing)

### 📚 Documentation

- f68641e data_structures: Improve docs on safety contract (#23575)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
camc314 added a commit that referenced this pull request Jul 3, 2026
## Memory reduction: fewer arena reallocations on JSX

`transform_jsx` built the `React.createElement` / jsx props object in a
**zero-capacity** arena vec
(`ctx.ast.vec()`), regrowing it (4 → 8 → 16 …) as it pushed one property
per attribute. The attribute
count is already known, so pre-sizing the vec to it removes those
regrowths — mirroring `arguments`,
pre-sized a few lines above, and the same pattern as #9915.

Measured with `cargo allocs` (`TestFiles::complicated`). **Arena
reallocations drop on every JSX file;
nothing else changes** (system allocs and the arena-alloc count are
essentially flat):

| file | arena reallocs: before → after | **reduction** |
| --- | --- | --- |
| App.tsx | 77 → 25 | **−52 (−68%)** |
| kitchen-sink.tsx | 517 → 304 | **−213 (−41%)** |
| RadixUIAdoptionSection.jsx | 24 → 13 | **−11 (−46%)** |

Each eliminated realloc is a buffer grow + memcpy of the existing
elements, so this is real
work removed on the JSX hot path, not just bytes.

<details><summary>Raw <code>allocs_transformer.snap</code> diff (updated
in this PR)</summary>

```diff
-App.tsx                    | 415.34 kB      ||             25 |              2 ||            719 |             77
+App.tsx                    | 415.34 kB      ||             25 |              2 ||            719 |             25
-RadixUIAdoptionSection.jsx | 2.52 kB        ||             21 |              2 ||            259 |             24
+RadixUIAdoptionSection.jsx | 2.52 kB        ||             21 |              2 ||            259 |             13
-kitchen-sink.tsx           | 732.90 kB      ||            184 |              3 ||           6689 |            517
+kitchen-sink.tsx           | 732.90 kB      ||            184 |              3 ||           6690 |            304
```

(Non-JSX files unchanged. The lone `+1` arena alloc on kitchen-sink is
an eager 1-slot allocation for
a rare element whose attributes are all filtered out — negligible
against −213 reallocs.)

</details>

## Change

```rust
let mut properties =
    ctx.ast.vec_with_capacity(opening_element.as_ref().map_or(0, |e| e.attributes.len()));
```

Capacity-only: the vec's contents, push order, and emitted output are
unchanged — only the initial
allocation differs. CodSpeed (instructions) may pick up the saved
memcpys on JSX-heavy input, but this
is framed as a memory/allocation win.

## Behavior-preserving

`cargo test -p oxc_transformer` passes. The change is a capacity hint
and cannot alter the produced AST.

---

Prepared with AI assistance.

Co-authored-by: Cameron <cameron.clark@hey.com>
camc314 pushed a commit that referenced this pull request Jul 3, 2026
…ering (#23516)

## What

Two zero-capacity arena `Vec`s in the TypeScript transformer were filled
in known-length loops and grew via reallocation. Pre-size them —
mirroring the JSX-props pre-size in #23466:

- **`transform_ts_enum_members`**: emits exactly `members.len() + 1`
statements (one per member + the final `return`) →
`vec_with_capacity(members.len() + 1)` (an **exact** capacity).
- **TS namespace `enter_program`**: rebuilds the program body; pre-size
to `program.body.len()` (an estimate — most statements pass through
1:1).

## Δ — fewer reallocations on TS fixtures

`allocs_transformer.snap`, reallocation count:

| fixture | before | after |
| --- | --- | --- |
| checker.ts | 64 | **5** |
| kitchen-sink.tsx | 304 | **280** |
| App.tsx | 25 | **17** |
| binder.ts | 6 | **0** |

(allocation **count** and **bytes** are unchanged — this removes the
intermediate growth reallocations.)

## Why it is behavior-preserving

Capacity-only: `vec_with_capacity` differs from `vec` solely in initial
capacity; element type, push order, and contents are identical, so the
produced AST is byte-identical. `oxc_transformer` tests pass.

Prepared with AI assistance.
camc314 pushed a commit that referenced this pull request Jul 3, 2026
### 💥 BREAKING CHANGES

- 7a76cd3 estree: [**BREAKING**] Make whether to include TS fields a
runtime option (#23574) (overlookmotel)
- e7b6b68 estree: [**BREAKING**] `ESTree` config use methods not consts
(#23573) (overlookmotel)

### 🚀 Features

- 556cc6d data_structures: Add `CodeBuffer::as_str` method (#23571)
(overlookmotel)
- 38c4b06 parser: Add friendly error for adjacent JSX elements (#23378)
(sapphi-red)
- 53509a8 minifier: Treeshake pure typed arrays and Set/Map array
literals (#23469) (Dunqing)
- 09762d9 minifier: Inline const value for read-only vars (#22593)
(Dunqing)

### 🐛 Bug Fixes

- 20375f9 react_compiler: Keep imports referenced only by a computed key
(#23586) (Boshen)
- 31bfd9b minifier: Keep Object introspection calls on a possible Proxy
(#23483) (Dunqing)
- 837a395 parser: Treat a line comment after ':' as leading, not
trailing (#23515) (Dunqing)
- e409fe0 minifier: Keep `new Map`/`WeakSet`/`WeakMap` with a string
argument (#23470) (Dunqing)
- ae02b4e ci/parser: Use `minimal` for vitest reporter (#23457)
(camc314)

### ⚡ Performance

- cf24329 mangler: Compile slot sort once instead of per CAPACITY
(#23577) (Boshen)
- 4058a6a parser: Reduce code bloat from verify_modifiers
monomorphization (#23576) (Boshen)
- 053b0c1 estree: Remove pointless `mem::take` (#23572) (overlookmotel)
- dfb52b6 transformer: Pre-size statement vecs in TS enum & namespace
lowering (#23516) (Yunfei He)
- 970e09a minifier: Compute template-literal inline checks in a single
pass (#23467) (Yunfei He)
- 3170c0e semantic,mangler,minifier: Fix `Semantic::stats` node count
and reuse stats in mangler builds (#23352) (Boshen)
- d1fa6e0 minifier: Evaluate ternary branches once in
minimize_conditional_expression (#23479) (Yunfei He)
- 3fa8051 transformer: Pre-size JSX props vec to attribute count
(#23466) (Yunfei He)
- 488b382 react_compiler: Borrow binding names in prefilter instead of
allocating (#23471) (Yunfei He)
- bcb3894 minifier: Incremental scoping refresh, delete
LiveUsageCollector (#23197) (Dunqing)

### 📚 Documentation

- f68641e data_structures: Improve docs on safety contract (#23575)
(overlookmotel)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-transformer Area - Transformer / Transpiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants