Bun Integrates the React Compiler Directly Into Its Bundler, Roughly 20x Faster Than the Babel Plugin

Bun Integrates the React Compiler Directly Into Its Bundler, Roughly 20x Faster Than the Babel Plugin

lschvn

Bun PR #32504, merged on June 20, 2026, turns the upstream React Compiler Rust port into a built-in bun build transform. Turn it on with --react-compiler from the CLI or reactCompiler: true on Bun.build, and Bun will memoize your .jsx and .tsx components and hooks during the build, with no Babel plugin, no config files, and nothing to install. The feature is off by default and marked experimental in both the type definitions and the bundler docs.

This is the first bundler to ship the React Compiler as a native transform. Vite, Next.js with Turbopack, webpack, and Rsbuild all run it through a Babel or SWC plugin today. Bun's path skips that intermediate entirely.

What landed

The integration closes issue #24356, the long-standing feature request for first-class React Compiler support in the bundler, and replaces an earlier PR #31785 that depended on an oxc_react_compiler crate that did not exist at the time. The new PR ports the compiler's Rust workspace directly from facebook/react rather than going through Oxc, which is why the upstream PR description in facebook/react#36173 explicitly invited bundler integrations via the react_compiler_oxc adapter and Bun took a different path.

A follow-up PR #32545 shipped the same day fixes three review comments from the merged PR, including a subtle bug where reactCompilerOutputMode: 'client' would silently enable the compiler even when reactCompiler: false was set. The output mode is now stored separately and only applied when the compiler is on, matching the documented behavior in bun.d.ts.

The architecture: Bun AST straight to HIR

The compiler lives in src/react_compiler/, a single ~62k LOC crate. The bulk of it is a byte-for-byte port of the upstream Rust workspace, with import paths rewritten and the serde and serde_json derives that Bun does not need stripped. The upstream workspace crates that are ported whole: hir/, ssa/, inference/, typeinference/, optimization/, validation/, reactive_scopes/, diagnostics/, and utils/. Hot-path data structures were densified: HashMap<SmallId, _> becomes Vec<_>, HashSet<ValueReason> becomes EnumSet (u16), and points-to sets become SmallVec<[_; 4]>. The IndexMap and IndexSet API is shimmed over arena-backed bun_collections::ArrayHashMap.

The four layers that touch the AST (lowering, codegen, pipeline, and the program/imports glue) are reimplemented against bun_ast, with the type-mapping table in src/react_compiler/DESIGN.md documenting how Bun's AST nodes correspond to the Babel-shaped AST the compiler expects.

The compile hook fires inside visit_stmts(FnBody), between its visit phase and its inline-mangle phase. Candidate detection on S::Function, S::Local, S::ExportDefault, and S::Expr records the binding Ref and the memo/forwardRef wrapper bit; visit_func and arrow-visit copy the function's args, flags, and locations into a Copy PendingCompile struct; the hook calls maybe_compile_pending, which constructs a stack-local G::Fn and runs maybe_compile_node. The compiled body lands in the live stmts buffer so the existing mangle phase runs on it. New arguments and flags flow back through a single CompileResult field. No raw pointers, no extra pass; the non-RC path adds one is_some() check per top-level declaration.

The compiler also honors // eslint-disable react-hooks/* suppressions. The lexer runs one substring check per comment, gated on the feature flag, and propagates the suppression as a flag bit on G::Fn and E::Arrow; the compiler skips any function carrying it.

The numbers

The PR ships a benchmark on a large React codebase (around 860 compiled components, 1400 memo slots). The same code, on the same machine:

Wall timevs Babel plugin
Baseline (reactCompiler: false)394 ms-
reactCompiler: true465 ms (1.18x baseline)~20x faster than Babel
Babel plugin (same input)9.15 s1x

The full --compile standalone executable build, which bundles everything plus the React Compiler pass, runs in 3.62 s with the Rust port versus 13.04 s with the Babel plugin, a 3.6x end-to-end speedup.

These are not synthetic micro-benchmarks. The codebase is real, the components compile to real _c(N) memoization calls with $[0] !== label cache checks, and the react/compiler-runtime import Bun injects resolves against the React 19+ install that ships with the app. Bun notes that the baseline-with-RC overhead (394 ms to 465 ms, ~18%) is from HIR construction and SSA pass; the rest of the bundler (parser, mangle, minify) is unchanged.

What the API looks like

CLI:

bun build ./app.tsx --react-compiler --target browser

Bun.build:

await Bun.build({
  entrypoints: ["./app.tsx"],
  reactCompiler: true,
  // reactCompilerOutputMode: "client", // default for browser target
  // reactCompilerOutputMode: "ssr",   // default for bun/node target
  target: "browser",
});

reactCompilerOutputMode defaults to "client" when target is "browser" and to "ssr" when target is "bun" or "node". SSR mode skips the useMemoCache runtime so server-rendered output stays cache-friendly across requests. compilationMode: "infer" semantics carry over from the upstream compiler, so only components and hooks are compiled; "use no memo" directives are honored, and node_modules is skipped.

What this means for the bundler race

This is the first time the Rust port of the React Compiler has shipped as a build-time transform rather than as a library other tools have to plug into. The Oxc v0.135 integration in mid-June added the compiler as a Rust crate you could call into, but the only bundler to actually wire it up since is Bun. Vite 8 and Vite 8.1 still go through babel-plugin-react-compiler; Next.js with Turbopack uses the SWC port; webpack uses Babel. Bun's choice to port upstream directly into its own AST layer is a deliberate trade: it skips the cross-AST conversion cost and the dependency surface, at the price of having to re-sync against facebook/react periodically.

The maintenance path is wired up. scripts/sync-react-compiler.sh sparse-fetches facebook/react and prints a per-file diff between src/react_compiler/UPSTREAM_PORTED and upstream tip, grouped into whole-crate ports that apply mechanically and AST-boundary ports that re-port via the type-mapping table. --fixtures re-syncs the test corpus. As long as the upstream API stays Babel-AST-shaped, the cost of tracking the port is roughly proportional to how often upstream touches the boundary layers.

Where to watch

Three signals worth tracking over the next few weeks:

  1. The Bun v1.3.15 release notes when they land, which should bundle PR #32504 plus the follow-up and promote the feature from bun build experimental to a stable flag.
  2. The Oxc react_compiler_oxc adapter landing as a stable crate in an Oxc release, which is the path Vite and Rolldown will most likely take to get the same perf numbers without porting upstream.
  3. Any change in the upstream React Compiler's "public API" from "Babel AST + scope info" to a more bundler-native shape, which would let Oxc (and through it Vite, Next.js, Rsbuild) skip their own adapter crates entirely.

Frequently Asked Questions

Related articles

More coverage with overlapping topics and tags.

Oxlint v1.72 and Oxfmt v0.57 Land the v0.138 Crates Cycle, Unify the AstBuilder, and Retire the Prettier CSS/GraphQL Fallback
tooling

Oxlint v1.72 and Oxfmt v0.57 Land the v0.138 Crates Cycle, Unify the AstBuilder, and Retire the Prettier CSS/GraphQL Fallback

Oxlint apps_v1.72.0 and oxfmt apps_v0.57.0, both published on 2026-06-29, close out the v0.138 crates cycle predicted in the [v1.71 release notes](/articles/2026-06-23--oxlint-v1-71-oxfmt-v0-56). The crates release (crates_v0.138.0, also 2026-06-29) unifies the old and new AstBuilder APIs (#23876, #23834, #23831, with legacy methods marked `#[deprecated]`), renames `AllocatorAccessor` to `GetAllocator` and switches its `allocator` method to take `&self` (#23675, #23676), makes `Str` and `Ident` methods take `&GetAllocator` (#23781), adds `transformer_plugins: Support typeof define keys` for vue-i18n and similar macros (#23605, Alexander Lichter), and ships the headline perf entry of the cycle: `minifier: memoize value_type to remove its O(n^2) re-walk on long binary chains` (#23929, Dunqing), which turns a 20k-term arithmetic-addition chain from 6,118 ms to 4.7 ms (~1300x) and a real-world antd.js bundle from 78.1 ms to 65.4 ms. Oxlint v1.72.0 ships 3 features (React `no-unknown-property` suggestion #23936, AstBuilder unification #23875, `eslint/no-restricted-import` schema #23642), 18 bug fixes, and 23 performance entries. Oxfmt v0.57.0 carries two BREAKING changes that retire the Prettier fallback for CSS/LESS/SCSS and GraphQL files: `Format parser:css,less,scss files + css-in-js by oxc_formatter_css` (#23321, leaysgur) and `Support draft syntax with removing prettier fallback` (#23326, leaysgur), both landing on top of the new `oxc_formatter_css` (#23320) and `oxc_formatter_graphql` (#23317) crates.
Deno 2.9 Ships 1.98x Faster Cold Start, 2.2-3.1x Less RSS Under Load, Default-On npm Minimum Release Age, No-Downgrade Trust Policy, and Built-In Snapshot Testing
runtimes

Deno 2.9 Ships 1.98x Faster Cold Start, 2.2-3.1x Less RSS Under Load, Default-On npm Minimum Release Age, No-Downgrade Trust Policy, and Built-In Snapshot Testing

Deno 2.9 (Bartek Iwańczuk, published 2026-06-25 on deno.com/blog/v2.9) is the largest Deno release of the cycle. Cold start drops from 34.2 ms to 17.3 ms (1.98x), peak RSS on the Deno.serve realworld workload drops 2.2x (142 MB → 64 MB) and 3.1x on 1 MiB bodies (197 MB → 63 MB), and Deno.serve throughput climbs 1.27x realworld (56.8k → 72.4k req/s), 1.11x plaintext, and 1.18x on 1 MiB bodies. Supply chain hardening: npm minimum-release-age is enabled by default with a 24h window (PR #35458), and a new opt-in no-downgrade trust policy (PR #34927) refuses to resolve any version whose trust evidence (staged publish, trusted publishing, provenance attestation) is weaker than the strongest evidence on any earlier-published version of the same package. Test runner parity: built-in t.assertSnapshot() (#35139), Deno.test.each (#34938), --shard for CI fan-out (#35057), retry and repeats (#35053), change-aware --changed and --related (#35199), and coverage thresholds (#35056). Lockfile interop: deno install seeds deno.lock from package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock (#34296, #35330, #35346, #35350, #35394), pnpm-workspace.yaml auto-migrates to deno.json / package.json (#34993), and git merge conflict markers in deno.lock auto-resolve (#34726). Plus: deno desktop graduates from experimental (the June 16 PR #33441), deno link / deno unlink / deno list / deno watch subcommands, stable --unsafe-proto (#34738), Web Locks API (#31166), Happy Eyeballs v2 (RFC 8305) (#31726), navigator.userAgentData (#34743), the WebCrypto Modern Algorithms proposal (ML-KEM, ML-DSA, SLH-DSA, ChaCha20-Poly1305, SHA-3 family, KMAC, Argon2) (#34447, #34448, #34914, #35223), Node 26.3.0 compat (#34746, #34747), Node-API v10 (#35270), and CSS module imports under --unstable-raw-imports (#35093). 165+ PRs land in this cycle.
Node.js 26.4.0 'Current' Ships node:vfs Subsystem (Matteo Collina), ESM Loader Package Maps (Maël Nison), TLS Certificate Compression, TCP_KEEPINTVL/TCP_KEEPCNT, and argon2 Stable
runtimes

Node.js 26.4.0 'Current' Ships node:vfs Subsystem (Matteo Collina), ESM Loader Package Maps (Maël Nison), TLS Certificate Compression, TCP_KEEPINTVL/TCP_KEEPCNT, and argon2 Stable

Node.js 26.4.0 (Current), published 2026-06-24 by @aduh95, lands eight SEMVER-MINOR changes: a minimal node:vfs subsystem that mounts user-supplied virtual filesystems (PR #63115, Matteo Collina) plus a follow-up that dispatches node:fs/promises to mounted VFS instances (PR #63537), package maps for ESM loaders that route bare specifiers through the loader hooks (PR #62239, Maël Nison), TLS certificateCompression that wires RFC 8879 zlib and zstd compression through the OpenSSL build config (PR #62217, Tim Perry), TCP_KEEPINTVL and TCP_KEEPCNT support in net.Socket.setKeepAlive (PR #63825, Guy Bedford), caller-supplied buffers in fs.readFile / fs.readFileSync (PR #63634, Matteo Collina), closeIdleConnections that now also drops pre-request sockets (PR #63470, semimikoh), net.BlockList advanced to Release Candidate stability (PR #63050), and crypto argon2 + KEM encap/decap marked stable (PR #63924, Filip Skokan). The release also adds WebCrypto cSHAKE (PR #63988), QUIC listEndpoints (PR #63536) and X509Certificate handles (PR #63191), dgram connectSync / bindSync (PRs #63838 + #63932, Guy Bedford), early-TCP net.BoundSocket (PR #63951), an experimental fast FFI call path for AArch64 and x86_64 (PRs #63068 + #63941, Paolo Insogna), npm 11.17.0 (PR #63857), sqlite 3.53.2, and libffi 3.6.0.

Comments

Log in Log in to join the conversation.

No comments yet. Be the first to share your thoughts.