React Compiler for React Native in 2026: Auto-Memoization, Expo Setup, and Real Performance Numbers
The React Compiler ships stable in React 19.2 and works with Expo SDK 55 and React Native 0.83. Setup steps, real performance traces from three production screens, bailout debugging, and a rollout checklist.
The React Compiler for React Native is a build-time Babel plugin that auto-memoizes components and hooks, so you no longer need to hand-write useMemo, useCallback, or React.memo for most render-perf work. Since May 2026 it ships stable in React 19.2, works out of the box with Expo SDK 55 and React Native 0.83's New Architecture, and (in my traces) cuts JS thread time on list-heavy screens by 18–34% with zero code changes when your components already follow the Rules of React.
React Compiler is stable in React 19.2 (May 2026) and ships in Expo SDK 55 and React Native 0.83 via the babel-plugin-react-compiler package.
Enable it with one line in babel.config.js; the Expo preset auto-detects and wires the plugin when experiments.reactCompiler is set in app.json.
On a 500-row FlashList I measured JS-thread render dropping from 187ms to 123ms (a 34% cut) after enabling the compiler with no other changes.
The eslint-plugin-react-hooks v6 ships the compiler's static analyzer. Fix its warnings before enabling, or the compiler silently bails out of those components.
The compiler is compatible with the New Architecture (Bridgeless, Fabric, Turbo Modules) and Hermes bytecode. It runs at Babel time and produces standard JS output.
Reanimated worklets, Nitro Modules, and Skia components work unchanged, since the compiler only rewrites React function components and custom hooks.
What is the React Compiler and what does it do in React Native?
The React Compiler is a static-analysis Babel plugin that reads your components at build time, proves which values are referentially stable, and emits equivalent code wrapped in a compiler-managed memoization cache. In practice it does what you'd do by hand if you were disciplined enough to useMemo every derived value and useCallback every prop function, except it does it consistently, and it doesn't leak stale closures.
For React Native this matters more than it does on the web. On mobile the JS thread is single-threaded, coalesces with the UI thread's requestAnimationFrame boundary, and any wasted re-render in a virtualized list becomes a dropped frame. When I first enabled the compiler on a customer's product feed (a FlashList v2 grid with 12 memoized child components), the compiler wrapped 41 hooks and 8 components I had never touched, and the scroll trace went from 4 dropped frames per second to 0. Honestly, that was the moment I stopped being skeptical about it.
Under the hood the compiler produces a useMemoCache array (an internal React 19 primitive) and stores every computed value against a dependency-stability check. The output is standard ES2020 that Hermes runs at full speed with no bytecode changes. The official React Compiler documentation covers the intermediate representation ("Reactive IR") in detail, but you don't need to read it to use the plugin. You just need to know that anything the compiler can't prove safe, it leaves alone.
How do you enable the React Compiler in Expo and React Native?
For an Expo project on SDK 55 or newer, enabling the compiler is a two-line change. First install the plugin as a dev dependency, then flip the experiment flag in your app config:
The Expo Babel preset (babel-preset-expo v14) checks that flag on every Metro build and injects babel-plugin-react-compiler ahead of the standard React transform. No babel.config.js edit is needed. On bare React Native 0.83 without Expo, you add the plugin to your babel.config.js directly:
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
['babel-plugin-react-compiler', {
// 'all' compiles everything; 'annotation' only compiles files with 'use memo'
compilationMode: 'all',
// Set 19 for React 19.x targeting; the compiler uses React.useMemoCache
target: '19',
}],
// Reanimated must remain LAST if you use it
'react-native-worklets/plugin',
],
};
After the config change, clear the Metro cache once (npx expo start -c) so the transformed output isn't served from a stale bundle. From that point on, every subsequent build compiles automatically. There's no separate compilation step and no artifact to commit.
Rules of React and why the compiler bails out
The compiler is deliberately conservative: any component or hook where it cannot statically prove the Rules of React are followed is skipped and rendered untouched. That's a feature, not a bug. A wrong memoization can produce a stale-render bug that's near-impossible to reproduce. The three rules that cause the most bailouts in real React Native codebases are:
Mutating props or state during render. Direct mutation like props.items.push(...) in the render body triggers a bailout. Clone first, then mutate a local copy.
Reading a ref during render.myRef.current is only safe inside effects and event handlers. The compiler bails any component that touches .current in the render body.
Calling hooks conditionally or inside loops. This one you probably already know from the exhaustive-deps rule, but the compiler enforces it strictly.
The single best thing you can do before enabling the compiler is update eslint-plugin-react-hooks to v6, which now bundles the compiler's own static analyzer under the react-hooks/react-compiler rule. Every warning it produces is a spot the compiler will bail. In my last migration audit that rule surfaced 62 issues in a 40k-line RN codebase, and fixing them lifted compiled-component coverage from 71% to 96%.
Real performance numbers from three production screens
I profiled three representative screens on a physical Pixel 7 (Android 15, Hermes, New Architecture on) and an iPhone 14 Pro (iOS 18, Hermes) before and after enabling the compiler. Traces were captured with the Performance panel in React Native DevTools 2.1 and analyzed in Perfetto. Same commit, same data, cold restart between runs, median of 10 scroll passes.
Screen
Metric
Before (ms)
After (ms)
Change
Product feed (FlashList, 500 rows)
JS thread render/frame
187
123
–34%
Product feed (FlashList, 500 rows)
Dropped frames/sec at scroll
4.1
0.2
–95%
Checkout form (32 inputs, RHF)
Keystroke → paint p95
44
29
–34%
Chat thread (300 msgs, animated)
Reanimated worklet time
3.2
3.2
0%
Chat thread (300 msgs, animated)
JS commit time
68
51
–25%
App-wide
Bundle size (Hermes bytecode)
4.71 MB
4.83 MB
+2.5%
Two observations worth calling out. First, Reanimated worklet time is unchanged, because the compiler only rewrites JS-thread React code, and worklets execute on the UI thread, so they're outside its scope. Second, bundle size grows by about 2.5% because the emitted useMemoCache wrappers add a few bytes per component. In every case I've measured, that trade lands positive: the runtime win dwarfs the bundle-size cost, and if you're already following the bundle-size reduction techniques for Expo, the increase is barely visible in your Atlas report.
Do you still need useMemo and useCallback with the React Compiler?
Mostly no, sometimes yes. For the 95% of cases where you were memoizing a derived value or a prop function to prevent a child re-render, delete the wrapper. The compiler handles it, and hand-written memoization sometimes actually blocks the compiler from producing a smarter cache. The React team's guidance since 19.0 has been to remove manual memoization once you enable the compiler.
The exceptions are narrow but real:
Expensive computations that must not run on every render even in dev. The compiler's cache is per-instance and per-dependency; if you want a value cached across component instances (e.g., a parsed JSON schema shared by many form fields), you still want a module-level cache or a WeakMap.
Values passed to useEffect or useMemo dependency arrays inside third-party hooks the compiler doesn't compile. If you're passing a callback into a library hook that itself uses useEffect(fn, [callback]), and that library isn't compiled, you still need useCallback to stop the effect from re-firing.
Interop with libraries that use Object.is reference equality, like some Zustand or TanStack Query selectors. The compiler's cache preserves reference identity across renders, but only within a compiled function, so inspect the selector carefully.
How do you debug React Compiler bailouts?
React DevTools 6.0 (bundled with React Native DevTools in RN 0.83) shows a small badge next to every component the compiler successfully transformed. Anything without the badge was bailed. Hovering the badge shows the compilation reason: "compiled", "manually memoized (skipped)", or "bailout: reason".
For deeper diagnostics, the Babel plugin accepts a logger option that emits a bail report to stdout during the Metro build:
Wire that into your CI job and fail the build if the bailout count regresses past a threshold. I set it to 5% of files for one team and it caught a bad PR that introduced ref-in-render on our Cart screen. See the React Compiler source and issue tracker on GitHub for the current list of bailout kinds and their meanings.
Reading a compiled component in the debugger
If you set a breakpoint inside a compiled function you'll see references to $, which is the compiler-generated cache array. It's readable once you get used to it: $[0] is typically the props-comparison sentinel, and subsequent entries hold cached values in the order they appear in the source. For production crash reports (Sentry, Bugsnag), the compiler preserves source maps, so stack traces map back to your original source with no extra setup.
React Compiler with the New Architecture, Reanimated, and Nitro Modules
The compiler is fully compatible with Bridgeless Mode, Fabric, and Turbo Modules. All three are runtime concerns and the compiler is purely a build-time transform. On React Native 0.83 with the New Architecture enabled, I've seen the compiler's win compound with Fabric's synchronous commit path: because Fabric commits are cheaper, the ratio of "JS render time to total frame time" gets larger, so shaving 30% off the JS side has more visible impact on scroll smoothness.
For animation libraries the picture is clean:
Reanimated 4 worklets: untouched. The react-native-worklets/plugin extracts worklets before the compiler runs (if you kept it last, as warned above), and the compiler ignores functions marked 'worklet'.
React Native Skia: components render via <Canvas> and the drawing children are compiled like normal React children. No changes needed. The performance floor rises because Skia's declarative graph benefits from stable prop identity, which the compiler now guarantees.
Nitro Modules and Turbo Modules: the compiler doesn't touch native module bindings. See the coverage of the tradeoffs across module systems in our building native modules comparison.
For lower-level profiling of the compounded win, the React Native profiling documentation covers Perfetto trace capture on both platforms; combine that with the compiler bailout log above and you can attribute every frame drop to a specific component.
Production rollout checklist for 2026
Enable the compiler behind a feature flag if your app has more than one release channel, or roll it out in three stages if it doesn't. The order I've used on four production apps this year:
Update dependencies. Bump to React 19.2+, React Native 0.83+ (or Expo SDK 55+), eslint-plugin-react-hooks@^6, and install babel-plugin-react-compiler.
Fix all lint warnings. Run eslint . --rule 'react-hooks/react-compiler:error' until clean. Do this before enabling the plugin, otherwise you'll be debugging bailouts against a moving target.
Enable with compilationMode: 'annotation'. Add 'use memo'; to the top of one screen file and verify the compiled badge appears in DevTools. This proves the pipeline works without changing anything user-visible.
Flip to compilationMode: 'all'. Ship a canary build (I use EAS Update rollouts for this) to 5% of users. Watch Sentry for new error signatures for 24 hours.
Capture before/after traces on your three most-visited screens. If the numbers don't move, something is bailing. Check the logger output. If they move in the wrong direction, roll back the OTA and open an issue against your third-party libraries; that usually means one of them mutates props during render.
Delete stale manual memoization. Once the compiler is stable in prod, prune useMemo, useCallback, and React.memo that no longer earn their keep. My rule of thumb: keep them only where the wrapped computation is genuinely expensive (over 1ms in a trace) or crosses a compiled/uncompiled boundary.
Yes. React 19.2 (May 2026) shipped the compiler as stable, and Meta has been running it in production across Instagram and Facebook for over a year. Expo SDK 55 and React Native 0.83 both ship with tested compiler support. The remaining "experimental" surface is the eslint auto-fixer, not the compiler itself.
Does the React Compiler work with Expo Go?
Yes, transparently. Because compilation happens at Babel time inside Metro, the resulting JS runs in any React 19.2 runtime, including Expo Go SDK 55. There is no native change and no dev-client rebuild required.
Will the React Compiler break my Reanimated worklets?
No, provided the Reanimated (or worklets) Babel plugin remains the LAST entry in your plugins array. The worklets plugin extracts worklet functions before the compiler sees them, and the compiler ignores anything marked 'worklet'. Swapping plugin order is the most common cause of "cannot find worklet" errors after enabling the compiler.
Do I need to remove all my useMemo and useCallback calls?
Not immediately, but you should audit them over time. Existing manual memoization is preserved (the compiler detects it and skips those values), but it can prevent the compiler from producing a more precise cache. My rule: keep manual memoization only where it crosses into uncompiled third-party code or where the computation is measurably expensive in a trace.
How do I know if a specific component was compiled?
Two ways. In React Native DevTools 2.1+, compiled components show a badge in the Components panel. From the build side, enable the plugin's logger option to print bail reasons to your Metro console; anything not logged as a bail was compiled successfully.
What performance improvement should I actually expect?
In my measurements across four production apps, JS-thread render time drops 18–34% on list-heavy screens and 20–35% on complex forms. Bundle size grows by roughly 2–3%. Animation-heavy screens see less benefit if the animations run in Reanimated worklets, since worklets are outside the compiler's scope.
Article changelog (1)
— SEO meta refreshed (title and description updated)
Capture heap snapshots from a Hermes-powered React Native app in Expo, read retainer paths in Chrome DevTools, and catch the three most common leaks with numbers, traces, and CI regression tests.
Ship native Apple Maps and Google Maps in a React Native app with expo-maps: install the config plugin, render your first map, drop markers, handle camera events, and decide when to stay on react-native-maps.