Skip to content

perf(react_compiler): make aliasing effects cheap to intern and clone#24506

Merged
Boshen merged 3 commits into
oxc-project:mainfrom
mds-ant:perf/react-compiler-effect-interning
Jul 14, 2026
Merged

perf(react_compiler): make aliasing effects cheap to intern and clone#24506
Boshen merged 3 commits into
oxc-project:mainfrom
mds-ant:perf/react-compiler-effect-interning

Conversation

@mds-ant

@mds-ant mds-ant commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

The mutation/aliasing inference interns every effect it applies. Both the interning map and the fixpoint-stable ValueId cache were keyed by a formatted String, so every lookup on the apply_effect hot path built a fresh string. On top of that, AliasingEffect::Apply carried an owned FunctionSignature, so cloning an Apply effect meant copying the whole struct including its parameter effect list, and apply_signature deep-cloned the cached effect list of every instruction it applied.

Change

  • Replace the String key with an EffectKey enum carrying exactly the fields the string encoded. Apply::signature/span and Mutate::reason still don't participate in identity, and the one collision the string format had (an empty Apply argument list vs a single hole) is reproduced on purpose. Building and hashing a key no longer allocates or formats, except in the rare error-carrying arms, which clone their message strings.
  • Put the FunctionSignature in Apply behind Rc. It's still deep-cloned out of the shape registry once per call instruction, but every later clone of the effect is just a refcount bump. The interning key already ignores the signature, and all readers are immutable.
  • Put the cached InstructionSignature entries behind Rc too, so apply_signature can hold the signature while passing the context on mutably, instead of cloning the whole effect list each time.

Follow-up to #24117; this PR is based on current main, which includes it.

Numbers from linting cal.com with only react/react-compiler enabled, single thread, fat-LTO release build:

Metric (cal.com, 1 thread) main this PR delta
instructions 19.113 G 18.086 G −5.4%
wall time 2.907 s 2.804 s −3.5%

Verification

  • Output of the main binary vs this PR's binary is byte-identical (stdout, stderr, exit code) on all 4 corpus/config combinations: cal.com and excalidraw, each with reportAllBailouts on and off.
  • The react_compiler example (parse, compile, codegen) produces identical output on all 1438 corpus files.
  • cargo test -p oxc_react_compiler passes, including the 1,736-fixture snapshot corpus: compiled output is unchanged on every fixture.
  • cargo clippy and cargo fmt clean.

One note for future re-syncs with the vendored React Compiler sources: EffectKey mirrors AliasingEffect variant for variant. Adding an identity-relevant field to a variant without updating effect_key silently changes interning behavior. Keep the two in sync.

This PR was assisted by Claude Code.

mds-ant added 3 commits July 14, 2026 10:16
Interning aliasing effects (and the fixpoint-stable ValueId cache)
hashed every effect by building a formatted `String` per lookup on the
hot path of `apply_effect`. Replace the string with an `EffectKey` enum
that carries exactly the same identifying fields — other fields such as
`Apply::signature`/`span` and `Mutate::reason` still do not participate
in identity, and the one collapse the string encoding had (an empty
`Apply` argument list vs a single hole) is reproduced explicitly.

Building and hashing a key no longer allocates or runs the formatting
machinery, except in the rare error-carrying arms which clone their
message strings.
… via Rc

`AliasingEffect::Apply` inlined an owned `FunctionSignature` — a struct
carrying several vectors and strings (builtin aliasing configs like
`Array.prototype.map` reach ~20 heap allocations) — so every clone of
an Apply effect on the fixpoint hot path was a multi-allocation deep
copy, and the enum's size was dominated by this one variant.

Wrap it in `Rc`: the signature is deep-cloned out of the shape registry
once per call instruction as before, and every subsequent effect clone
bumps a refcount. Interning keys already ignore the signature field,
and all readers access it immutably.
`apply_signature` cloned the cached signature's whole effect list on
every instruction application because the cache borrow conflicts with
passing the context on mutably. Wrap cache entries in `Rc` so holding
the signature across the loop is a reference-count bump instead of a
deep clone.
@mds-ant

mds-ant commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@Boshen Here's the follow-up PR I promised!

@codspeed-hq

codspeed-hq Bot commented Jul 14, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 7.26%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
✅ 7 untouched benchmarks
⏩ 61 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation react_compiler[RadixUIAdoptionSection.jsx] 2.9 ms 2.7 ms +9.91%
Simulation linter[RadixUIAdoptionSection.jsx] 3.4 ms 3.2 ms +7.64%
Simulation react_compiler[kitchen-sink.tsx] 201.9 ms 193.5 ms +4.31%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing mds-ant:perf/react-compiler-effect-interning (fa9662a) with main (c517aa0)

Open in CodSpeed

Footnotes

  1. 61 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.

@Boshen
Boshen merged commit 7808a6e into oxc-project:main Jul 14, 2026
30 checks passed
camc314 added a commit that referenced this pull request Jul 21, 2026
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 4c71560 parser: More friendly error for spread element in dynamic
imports (#24705) (sapphi-red)
- 7b045cd minfier: Drop last break from last switch case (#24673)
(Armano)
- 7d3c178 minifier: Remove unreachable recursive functions (#24125)
(Dunqing)
- 94f99b3 ast: Allow `NONE` to be passed to AST builder methods where
`Option<ArenaVec>` is expected (#24629) (overlookmotel)
- 77230c5 ast: Accept arrays for `ArenaVec` params of AST builder
methods (#24621) (overlookmotel)
- f08b152 allocator: Implement `FromIn` for array to `Vec` conversion
(#24620) (overlookmotel)
- 2338c13 track-memory-allocations: Track heap deallocs, alloc bytes,
and peak growth (#24619) (Boshen)
- 7aa4739 syntax,transformer: Move JSX entity decoder to `oxc_syntax`
(#24617) (camc314)
- 2b097c4 str: Export `Str` as `ArenaStr` (#24604) (overlookmotel)
- 3acf4c1 minifier: Expand switch optimiation to remove empty cases
(#24520) (Armano)
- 129b759 parser: Improve diagnostics for unparenthesized LHS on
exponential expr (#24569) (camc314)
- 4d0c601 minifier: Fold arithmetic over undefined and null operands
(#24485) (Dunqing)
- 91541dd minifier: Drop empty switch statements  (#24527) (Armano)
- d05224d ast_visit: Generate VisitJs visitor that skips TypeScript
type-space (#24499) (Boshen)
- 3d22307 parser: Add `ParseOptions::enable_ident_hashes` (#24491)
(Boshen)

### 🐛 Bug Fixes

- 64c2241 minifier: Align class heritage removal with assumptions
(#24533) (Dunqing)
- 48b59f4 parser: Span ambient generator diagnostics (#24711) (camc314)
- e750a82 ecmascript: Fix false negative for may_have_side_effects on
dynamic property access (#24709) (sapphi-red)
- f145d73 minifier: Guard reordered identifier reads (#24698) (Dunqing)
- a2ef382 isolated-declarations: Reject `window.Symbol` as global symbol
reference (#24689) (camc314)
- b1bcf72 minifier: Invalidate facts for redeclared bindings (#24658)
(Dunqing)
- 921b834 minifier: Don't treat a conditionally-assigned var as
write-once (#24650) (Dunqing)
- 061af1f minifier: Avoid stale pure function summaries (#24636)
(Dunqing)
- 40c2f43 allocator: `Vec::from_array_in` do not allocate zero-length
array (#24628) (overlookmotel)
- 70994ae codegen: Preserve comments before expression operands (#24510)
(Dunqing)
- 7b4baff parser: Reject new import member access (#23459) (camc314)
- 128b385 minifier: Clippy warning with no-debug-assertions (#24547)
(camc314)
- 8421feb parser: Use first `as` span for imported name (#24537)
(leaysgur)
- c517aa0 parser: Reject invalid accessor assertions (#24504) (camc314)

### ⚡ Performance

- 884d9eb parser: Pre-size cover-grammar assignment target buffers
(#24683) (Boshen)
- d3f07a0 diagnostics: Box OxcDiagnosticInner to reduce binary size
(#24665) (Boshen)
- bcc9de0 parser: Defer diagnostic creation until parse exit (#24663)
(Boshen)
- c35d8ab allocator: Mark `ReplaceWith` panic path cold (#24515)
(camc314)
- ba65790 semantic, allocator: Branchless `clone_in` for semantic IDs
(#24564) (overlookmotel)
- 747feec parser: Build AST nodes with the AST builder instead of
cloning (#24540) (Boshen)
- ba35a0d react_compiler: Use IndexVec for dense id-keyed maps (#24549)
(Boshen)
- b685062 react_compiler: Keep small hot-path collections inline
(#24514) (Marius Schulz)
- a149e95 transformer: Outline rare expression exits (#24512) (camc314)
- 7808a6e react_compiler: Make aliasing effects cheap to intern and
clone (#24506) (Marius Schulz)
- 3a36f2a react_compiler: Store AbstractValue reasons as a u16 bitmask
(#24480) (Boshen)
- 1c96753 react_compiler: Use FxHashMap for the lookup-only aliasing
node map (#24490) (Boshen)

Co-authored-by: Cameron <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants