feat(react_compiler): integrate the Rust port of the React Compiler#22942
Conversation
Merging this PR will not alter performance
Performance Changes
Comparing Footnotes
|
|
Excited to see this! Happy to help test if you need |
|
Yay! |
2a64d3a to
efd5480
Compare
|
Can't wait for this! 🚀 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: efd5480d2c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75d8ab9e72
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
5b696f1 to
c20a85e
Compare
Run the Rust port of the React Compiler (oxc-project/oxc#22942) at the pre-ecma-ast stage, gated by a new `transform.reactCompiler` option. - pin the oxc crates to the integration PR via [patch.crates-io], and add the oxc_react_compiler git dependency - add `react_compiler` to the bundler transform options; run it on the pristine AST in pre_process_ecma_ast before the oxc transformer, returning the scoping the rest of the pipeline uses - expose it as `reactCompiler?: boolean | ReactCompilerOptions` through the napi binding (reusing oxc_transform_napi's resolver) and the TS types - add a snapshot integration test (enabled vs disabled) Depends on oxc-project/oxc#22942; the patch pins a git rev until it lands.
Binary-size root-cause analysisFollowing up on the +5.14 MiB the napi addon gains (3.51 → 8.66 MiB, release/stripped) — I dug into where it comes from. ( cargo bloat
|
| crate | .text |
note |
|---|---|---|
std |
904 KiB | inflated by monomorphization from serde + compiler generics |
react_compiler_ast |
829 KiB | ~80% of it (0.65 MiB) is serde Serialize/Deserialize |
serde |
461 KiB | |
react_compiler_inference |
363 KiB | compiler pass |
oxc_parser |
322 KiB | baseline (parsing input) |
react_compiler_reactive_scopes |
316 KiB | compiler pass |
react_compiler_hir / _validation / _lowering / _optimization / … |
~0.9 MiB combined | compiler passes |
regex_automata + regex_syntax + aho_corasick |
~454 KiB | the regex crate |
serde_core |
228 KiB | |
oxc_react_compiler (the 6k-LOC convert_ast/reverse) |
191 KiB | smaller than expected |
serde_json |
68 KiB |
Transitive deps the React Compiler drags in (confirmed RC-only via cargo tree -i)
- serde + serde_core + serde_json ≈ 757 KiB — from every
forked_react_compiler*crate. regexengine ≈ 454 KiB — sole source isforked_react_compiler(not in the baseline transform).indexmap≈ 37 KiB,sha2≈ 8 KiB (fromreactive_scopes).- (
num_bigintis baseline oxc viaoxc_ecmascript— not the React Compiler.)
Root cause
The Rust port kept the JS plugin's interface: compile_program hands its result back as a JSON-serialized Babel AST that oxc_react_compiler immediately re-parses in the same process —
let result = react_compiler::…::compile_program(file, scope_info, options); // -> JSON string (RawValue)
let value: serde_json::Value = serde_json::from_str(raw_json.get()).ok()?; // parse JSON
serde_json::from_value(value).ok() // deserialize back to FileThat forces a full Serialize impl for the whole Babel AST (to emit the JSON) and a full Deserialize impl (to read it). On a type that large, that derive pair is why react_compiler_ast is 80% serde and why serde/serde_json get pulled in.
Takeaways (~half of the bloat is reducible, none of it in oxc itself)
- ~1.5 MiB is the genuine compiler-pass logic (HIR, inference, reactive scopes, validation, lowering, …) — inherent.
- ~1.2 MiB is reducible third-party transitive code, both fixable upstream in
forked_react_compiler:- serde/JSON round-trip (~757 KiB): have
compile_programreturn the Rustreact_compiler_ast::Filedirectly instead of a JSON string — both ends are in-process Rust, so theRawValue → Value → Fileround-trip is pure overhead (it's runtime cost too, not just size). regexengine (~454 KiB): I couldn't find aRegex::use in the crate's ownsrc; worth confirming it's actually exercised, and if its usage is light,regex-lite/ hand-rolled matching would reclaim most of it.
- serde/JSON round-trip (~757 KiB): have
|
@Boshen I don't know if they would act upon it, but is it worth passing this upstream to the React team? Amazing work to you and the React team by the way, I can't wait to use this! |
Bumped
|
| size | |
|---|---|
| before (0.1.0) | 8.66 MiB (9,076,384 bytes) |
| after (0.1.1) | 7.80 MiB (8,182,640 bytes) |
| delta | −0.85 MiB (−873 KiB, −9.8%) |
Where it went — cargo bloat on the oxc_react_compiler example
| crate | 0.1.0 | 0.1.1 |
|---|---|---|
regex_automata + regex_syntax + aho_corasick + regex |
~456 KiB | removed |
regex_lite |
— | 36 KiB |
react_compiler_ast |
829 KiB | 781 KiB |
.text total |
5.7 MiB | 5.2 MiB |
The ~456 KiB regex engine — plus its std monomorphization, which is why the addon drops more than the raw ~420 KiB crate delta — is replaced by 36 KiB of regex-lite. This realizes the larger of the two reductions flagged earlier. The serde/JSON round-trip (~757 KiB) remains the next reducible chunk, and would need the upstream compile_program to return the Rust File directly instead of a JSON string.
Removing the JSON round-trip — perf
Wall-clock
|
| input | before (0.1.1, JSON round-trip) | after (0.1.2, typed File) |
delta |
|---|---|---|---|
| App.tsx (405 KiB) | 14.95 ms | 10.83 ms | −4.13 ms (−28%) |
| RadixUIAdoptionSection.jsx (2.5 KiB) | 1.79 ms | 1.68 ms | −0.10 ms (−6%) |
The React Compiler's added overhead on App.tsx (with-RC − without-RC) drops roughly in half (~10.8 → ~5.8 ms). The without-RC baseline jitters ~1 ms run-to-run (it never touches the compiler), so read the overhead as "≈ halved" — but the with-RC totals above are direct measurements and the −4.1 ms gap is well clear of the noise.
Why it scales with file size: the round-trip serialized the entire program's Babel AST to a JSON string, parsed it to a serde_json::Value (to de-dup duplicate "type" keys from BaseNode.node_type + #[serde(tag = "type")]), then deserialized to File — all in the same process. That work is proportional to AST size, so it pays off most on large files and is negligible on small ones. Output is byte-identical.
Binary size (same change)
With nothing left serializing/deserializing the Babel AST, fat-LTO drops the now-dead react_compiler_ast serde code:
| napi transform addon (release/stripped) | size |
|---|---|
| before (0.1.1) | 7.80 MiB (8,182,640 B) |
| after (0.1.2) | 7.06 MiB (7,405,280 B) |
| delta | −759 KiB (−0.74 MiB, −9.5%) |
Cumulative with the earlier regex → regex-lite swap, both reducible clusters are now gone: 8.66 MiB (0.1.0) → 7.06 MiB (0.1.2), −1.60 MiB (−18.5%) — the rest is genuine compiler logic.
4b0e605 to
911e134
Compare
|
AI-assisted analysis update. I ran the React Compiler output comparison across all ecosystem CI JS/TS files under Summary:
High-signal OXC-only corruption buckets are now empty:
The remaining
One semantic-looking remaining diff needs follow-up:
Local verification already run and passed:
|
|
AI-assisted follow-up: pushed Root cause for
Fix:
Additional tests now cover:
Verification:
Suggested next step: rerun the full ecosystem comparison on current head, since the previous full compare was before the source-span fix. |
b9ca275 to
5fa5cce
Compare
Why the converter does JSON conversion for TypeScriptA heads-up for review, since it looks unusual. The upstream Babel AST ( pub type_annotation: Option<Box<serde_json::Value>>,
pub type_parameters: Option<Box<serde_json::Value>>,
pub return_type: Option<Box<serde_json::Value>>,
// TSAsExpression / TSSatisfiesExpression / TSTypeAssertion: type_annotation: Box<serde_json::Value>The React Compiler analyzes JavaScript value/runtime semantics (memoization, reactivity) and never inspects types, so the fork keeps them as pass-through JSON instead of modeling the full TS type AST. Everything else in the Babel AST is fully typed — only the type-system fields are JSON. Because oxc's AST is fully typed, the converter has to translate at the boundary:
Types have to survive this round-trip because the compiled output is handed to the downstream TypeScript transform, even though the compiler itself ignores them. This is only the per-type-field JSON. The whole- |
599e195 to
cb9681f
Compare
Merge activity
|
…22942) Closes #10048 Integrates the Rust port of the React Compiler ([react/react#36173](react/react#36173)) into oxc. ## Notes (resolved) - Published crates can't reference Git URLs, so using this from Rolldown needs the React Compiler crates on crates.io. ✅ Published as a fork at https://crates.io/crates/forked_react_compiler; this PR depends on those `forked_react_compiler*` crates so people can get earlier access. - The React Compiler crates had no license field. ✅ The published fork carries `license = "MIT"` (per the React repo's MIT license), so Cargo Deny and Security Analysis pass. ## Benchmark **Wall-clock overhead vs plain transform** (release `transformSync`, same `jsx: automatic`, toggling only `reactCompiler`): | fixture | without | with | overhead | | --- | --- | --- | --- | | RadixUIAdoptionSection.jsx (2.5 KiB) | 0.03 ms | 1.81 ms | +1.8 ms | | excalidraw `App.tsx` (406 KiB) | 4.09 ms | 14.49 ms | +10.4 ms (3.5×) | So roughly a fixed ~1.8 ms floor per file plus a per-size cost — about **3.5× the transform time** on a large real-world component. ## Binary size Linking the React Compiler pulls the whole compiler pipeline (HIR, lowering, inference, SSA, optimization, reactive scopes, validation) plus the oxc⇄Babel AST conversion into the binding. Release build of the napi transform addon (`transform.darwin-arm64.node`, `--release`, stripped), darwin-arm64: | build | size | | --- | --- | | baseline (`main`) | 3.51 MiB | | with React Compiler | 8.66 MiB | | **delta** | **+5.14 MiB (+146%, 2.46×)** |
cb9681f to
b846ab2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb9681ffa3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💥 BREAKING CHANGES - ee4dc73 ast: [**BREAKING**] Add `#[non_exhaustive]` to AST nodes (#23046) (overlookmotel) - 4c35362 ast: [**BREAKING**] Add `AstBuilder::template_element_escape_raw` and `template_element_escape_raw_with_lone_surrogates` methods (#23047) (overlookmotel) ### 🚀 Features - b846ab2 react_compiler: Integrate the Rust port of the React Compiler (#22942) (Boshen) - 5b8dd68 parser: Report TS1255 for invalid class definite assertions (#22917) (camc314) - 85efabf semantic: Make building the class table optional, off by default (#22862) (Boshen) ### 🐛 Bug Fixes - 556acdc codegen: Parenthesize TS-cast assignment targets (#23112) (Boshen) - 37169ff codegen: Don't emit space between postfix `--` and `>` when minifying (#23036) (Boshen) - a4b1bf7 codegen: Drop redundant whitespace in minified TypeScript output (#23038) (Boshen) - cf53285 parser: Report reserved type-declaration names in the parser (#23035) (Boshen) - 4e44969 ast: Fix UB in `escape_template_element_raw` (#23052) (overlookmotel) - c543154 parser: Report comma operator in JSX expression in the parser (#23030) (Boshen) - 325c94f codegen: Tighten conditional-type and constructor-type whitespace when minifying (#23033) (Boshen) - 95dd3a2 parser: Report `import type` alias to a non-external reference in the parser (#23032) (Boshen) - 90180b8 codegen: Drop space after `:` in function return type when minifying (#23028) (Boshen) - 6da876e parser: Report `abstract` private class field in the parser (#23029) (Boshen) - 28467ce codegen: Don't emit space before a postfix update operand when minifying (#23027) (Boshen) - cb29926 codegen: Drop redundant space after `export default` when minifying (#23024) (Boshen) - 62965ae codegen: Drop redundant space after `else` when minifying (#23025) (Boshen) - 989230a parser: Report compound assignment to non-simple target in the parser (#23022) (Boshen) - 06f367c parser: Report `super.#field` private access in the parser (#23014) (Boshen) - 184edef codegen: Print space before `const`/`declare` enum modifier (#23013) (Boshen) - 4d722e0 parser: Report duplicate switch `default` clause in the parser (#23012) (Boshen) - 597ed85 codegen: Parenthesize `let`/`async` for-of head target (#23008) (Boshen) - 8b631bf codegen: Remove stray space before mapped type value colon (#23010) (Boshen) - c08407e codegen: Don't over-parenthesize `in` inside an arrow in a for-init (#23009) (Boshen) - 600cd6f codegen: Parenthesize lower-precedence `TSInstantiationExpression` operand (#23007) (Boshen) - 187e1a5 codegen: Don't leak space after comment-only JSX expression container (#23006) (Boshen) - 294c473 codegen: Don't over-parenthesize `TSTypeAssertion` operand (#23004) (Boshen) - 786d96f codegen: Give `TSTypeAssertion` unary precedence (#23002) (Boshen) - 1295882 parser: Report `new.target` and `import.meta` syntax errors in the parser (#23003) (Boshen) - d727b6b codegen: Parenthesize `await` expression as base of `**` (#23001) (Boshen) - 67dfa08 codegen: Keep parentheses around `new` callees containing a call (#22997) (Boshen) - 17e7cf3 parser: Disallow unerasable `as`/`satisfies` assertions (#22986) (Boshen) - beb46d3 parser: Commit to module goal on decorated exports (#22941) (Boshen) - 49e63f7 isolated-declarations: Require annotations for satisfies initializers (#22898) (camc314) - 8c93601 isolated-declarations: Allow unknown enum initializer in non-const enum (#22900) (camc314) ### ⚡ Performance - 7d89909 parser: Peek instead of lookahead for yield disambiguation (#23071) (Boshen) - bf872f0 parser: Skip arrow lookahead for a parenthesized literal (#23070) (Boshen) - d19fc54 parser: Guard type-argument speculation behind an angle-token check (#23069) (Boshen) - 8eb5507 parser: Skip redundant member-rest re-scan on call entry (#23068) (Boshen) - 883dfc1 parser: Skip parse_call_expression_rest when no call follows (#23063) (Boshen) - b171153 parser: Peek before the await-using lookahead (#23059) (Boshen) - 56f21bd parser: Use peek_token for the TS `asserts` type predicate (#23058) (Boshen) - 68805ac parser: Use peek_token instead of checkpoint/rewind for single-token decisions (#23056) (Boshen) - 1f9d8eb ast: `AstBuilder::template_element_escape_raw` avoid allocation if no escape required (#23053) (overlookmotel) - 502b04d semantic: Move cold function redeclaration handling into `#[cold]` function (#22973) (overlookmotel) ### 📚 Documentation - 275d318 napi/minifier: Point `target` to oxc docs (#23102) (camc314) Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Closes #10048
Integrates the Rust port of the React Compiler (facebook/react#36173) into oxc.
Notes (resolved)
forked_react_compiler*crates so people can get earlier access.license = "MIT"(per the React repo's MIT license), so Cargo Deny and Security Analysis pass.Benchmark
Wall-clock overhead vs plain transform (release
transformSync, samejsx: automatic, toggling onlyreactCompiler):App.tsx(406 KiB)So roughly a fixed ~1.8 ms floor per file plus a per-size cost — about 3.5× the transform time on a large real-world component.
Binary size
Linking the React Compiler pulls the whole compiler pipeline (HIR, lowering, inference, SSA, optimization, reactive scopes, validation) plus the oxc⇄Babel AST conversion into the binding. Release build of the napi transform addon (
transform.darwin-arm64.node,--release, stripped), darwin-arm64:main)