The React Compiler in Next.js
Turn on reactCompiler in a Next.js app, see which components it memoizes and which code it leaves alone, opt out with use no memo, and check its work.
- 01Turn the compiler onInstall its Babel plugin, set
reactCompilerin next.config.ts, and know what it costs at build time. - 02Say what it compilesTell which code Next.js gives the compiler (Client Component code built for the browser) and what that code keeps between renders.
- 03Opt out, opt in, and checkUse
"use no memo", annotation mode and"use memo", and check the result with React DevTools, ESLint and the built JavaScript.
A tea list that stops rendering again
The tea shop's order page has a note box and a list of teas. When you type in the box, React renders the form again, and by default every component the form renders, too (When React re-renders shows why). The React Compiler is a build tool from the React team that adds memoization to your components for you: it rewrites each one so it keeps the values, functions and JSX elements it can, and makes each one again only when what it was made from changes. The component itself still runs on every render; a child can be skipped when the parent hands it the same element as last time. The React Compiler, in the React course, shows the code it writes. Next.js has it built in, as one option in next.config.ts.
Here is the page without the compiler. It's a real Next.js 16.4.3 project: it was built with next build, run with next start, and opened in real Chrome, which typed a note. Each part of the page counts its own renders, so you can see what React did.
app/order-form.tsx"use client"; import { useState } from "react";import TeaList from "./tea-list";import { useRenderCount } from "./use-render-count"; const teas = ["Green tea", "Mint tea", "Lemon tea"]; export default function OrderForm() { const [note, setNote] = useState(""); const countRef = useRenderCount(); return ( <section> <label htmlFor="note">Note</label> <input id="note" value={note} onChange={(event) => setNote(event.target.value)} /> <p> OrderForm renders: <span ref={countRef} /> </p> <TeaList teas={teas} /> </section> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
OrderForm renders: 2
- Green tea
- Mint tea
- Lemon tea
TeaList renders: 2
- Focus
- the <input>
- Scrolled
- 0 px down
No requests during this step: the page already had what it needed
app/order-form.tsx starts with "use client", so OrderForm is a Client Component (Client Components). Line 10 keeps the note in state, and line 15's onChange sets it on every change. Line 19 renders TeaList with the teas array from line 7, which is made once, when the file loads, so it's the same array on every render.
Both components call useRenderCount from app/use-render-count.ts. Its Effect, on lines 8 to 11 of that file, has no list of dependencies, so it runs after every render of the component that calls it. Line 9 adds one to the count, and line 10 writes it into the <span> that gets the ref. It touches the refs only inside the Effect, never while rendering, which is what the compiler needs.
Open the Steps tab. Step 1 opened /: both counts say 1. In step 2, Chrome typed Oolong, please into Note, all at once, as one change. OrderForm renders: 2, and TeaList renders: 2. TeaList's props didn't change, but React called it anyway, because OrderForm returned a new <TeaList> element. Now the same page with the compiler on:
next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ cacheComponents: true, partialPrefetching: true, reactCompiler: true,}; export default nextConfig;Each step was taken in real Chrome against next start. Pick one to see the page after it.
OrderForm renders: 2
- Green tea
- Mint tea
- Lemon tea
TeaList renders: 1
- Focus
- the <input>
- Scrolled
- 0 px down
No requests during this step: the page already had what it needed
Two files changed, and no component did. Line 7 of next.config.ts turns the compiler on, and line 19 of package.json installs its Babel plugin. In the Steps tab, step 2 says OrderForm renders: 2 and TeaList renders: 1. The compiled OrderForm kept the <TeaList> element it made on the first render, because nothing it was made from had changed, and returned that same element. React skips a component whose element is the very same object as last time. No memo anywhere (memo, useMemo and useCallback is what you'd write by hand).
OrderForm renders again in both. Only the compiled OrderForm returns the <TeaList> element it kept, and React skips a component whose element is the same as last time.A restaurant hires an editor to go through its recipes before the doors open. The head chef decides which books she gets: the dining-room recipes, which the cooks make again and again during service, but not the morning prep lists, which are done once. The cooks then follow her notes and skip repeated work. A recipe with a "don't mark this one" note stays as it was.
- In real life: The head chef, who runs the kitchen and decides what goes where
- In Next.js: Next.js, which runs the build
- In real life: An editor who marks up recipes before service: "if the order is the same, serve the plate already made"
- In Next.js: The React Compiler, which adds a memo cache to each component
- In real life: The dining-room recipes, cooked again and again during service
- In Next.js: Client Components, built for the browser, where they render again and again
- In real life: The prep lists, done once in the morning
- In Next.js: Server Components, which render once per request or build
- In real life: A sticky note on one recipe: "don't mark this one"
- In Next.js:
"use no memo"as a component's first line
Install babel-plugin-react-compiler and set reactCompiler: true, and Next.js runs the React Compiler on your Client Component code as it builds it for the browser. Compiled components and Hooks keep the values, functions and JSX elements they can, and reuse each one until what it was made from changes; a child whose element is reused isn't rendered again. Server Components and packages in node_modules are left as written.
Turning it on in next.config.ts
The compiler is a Babel plugin: a piece of Babel, a tool that rewrites JavaScript as your app is built. It doesn't come with the next package. In an existing app, add it as a dev dependency, a package the build uses but visitors never download:
npm install --save-dev --save-exact babel-plugin-react-compiler--save-exact writes the exact version, with no ^, so an upgrade of the compiler happens only when you choose it. On October 30, 2026, the latest version was 1.0.0. Then set the option, as line 7 of the last example's next.config.ts does. A new app can start with both: create-next-app asks Would you like to use React Compiler?
(the default answer is No), or takes a flag:
npx create-next-app@latest tea-shop --react-compilerIt adds "babel-plugin-react-compiler": "1.0.0" to package.json and reactCompiler: true to next.config.ts, the two changes in the example. Each row is one way to write the option, what it does, and which Next.js has it:
| What you write in next.config.ts | What it does | Where it works |
|---|---|---|
reactCompiler: true | Compiles every component and Hook in the browser code that it can (infer mode). | Next.js 16 and later. |
reactCompiler: { compilationMode: "annotation" } | Compiles only the functions that start with "use memo". | Next.js 16 and later. |
experimental: { reactCompiler: true } | The same as reactCompiler: true, before the option became stable. | Next.js 15. |
experimental: { turbopackRustReactCompiler: true }, with reactCompiler | Runs a Rust port of the compiler inside Turbopack instead of Babel. | Next.js 16.3 and later, experimental. |
The option became stable in Next.js 16. In Next.js 15 it was experimental.reactCompiler, and older tutorials still show that spelling. Set the option without installing the plugin, and the build stops:
next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ cacheComponents: true, partialPrefetching: true, reactCompiler: true,}; export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ... > Build error occurredError: Turbopack build failed with 1 error:./next.config.tsError: Failed to resolve package babel-plugin-react-compiler while attempting to resolve React CompilerReact compiler is enabled in next.config.ts. We attempted to resolve React Compiler relative to the next package. Is babel-plugin-react-compiler installed in your node_modules directory? at ignore-listed framesThe Terminal tab shows next build stopping on ./next.config.ts: Failed to resolve package babel-plugin-react-compiler while attempting to resolve React Compiler
. Next.js looks for the plugin next to its own package, in node_modules, and asks whether it's installed. Install it, and the build passes, as in the last example.
What it costs. Next.js's own compiler, SWC, is written in Rust and is fast. The React Compiler runs through Babel, in JavaScript, so each file it compiles takes more work. Next.js's blog post for version 16 says to expect longer compile times in next dev and next build with the option on, and that it isn't on by default while the team gathers build data from different kinds of apps. Next.js keeps the cost small by giving the compiler only the code that needs it: browser code, after a quick check of each file. The recordings don't measure it: they hide times, which change from run to run.
Next.js 16.3 added an experimental Rust port of the compiler that runs inside Turbopack instead of Babel: experimental: { turbopackRustReactCompiler: true }, next to reactCompiler. Next.js 16.4 made it faster, and it was still marked experimental in 16.4. The recordings use the stable Babel plugin. There's nothing to install for the browser: compiled code imports its cache from react/compiler-runtime, which comes with React 19.
Which code Next.js compiles
next build builds your code twice: once for the browser, and once for the server. The server's copy holds the Server Components and also each Client Component, which renders there to make the first HTML. Next.js 16.4.3 hands the compiler only the browser's copy. This is the rule Turbopack uses to decide, from Next.js's Rust code:
// bail early: no babel config, no react-compiler, or Rust React Compiler is active (babel has
// nothing to do).
if babel_config_path.is_none()
&& (react_compiler_options.is_none()
|| use_rust_react_compiler
|| !builtin_conditions.contains(&WebpackLoaderBuiltinCondition::Browser))
{
return Ok(Vec::new());
}Read it from the top. With no Babel config file of your own, Next.js adds no Babel step at all when the compiler is off, when the experimental Rust port is on, or when the code isn't being built for the browser: that last line, !builtin_conditions.contains(&WebpackLoaderBuiltinCondition::Browser), is "not for the browser". You can read it in babel.rs, lines 123 to 131. Then three more filters run before a file reaches the compiler:
- Files in
node_modulesare skipped (get-config.ts, lines 119 to 126), and so is any build for the server. - A quick text check: in the default mode, the file must hold
"use memo", a name likeuseState(usethen a capital letter),</or/>(babel.rs, lines 245 to 257). A file of plain helpers never pays for Babel. - The compiler itself then compiles only functions that look like components (a name with a capital first letter) or Hooks (a name that starts with
use).
next build builds the code twice: once for the browser, once for the server. Next.js 16.4.3 gives the compiler only the browser's copy (shaded). The Server Components and the server's copy of the Client Components render once per request or build, so they're left as written.Why leave the server alone? A Server Component renders once for each request, or once during the build, and its result is sent as HTML and the RSC payload. It never renders again in the browser, so there's no second render for a memo cache to save (Server Components in Next.js). The server's copy of a Client Component also renders once per request. The renders that repeat are in the browser, after hydration, when state changes.
You can see it in the recordings. Each one looked for some text in the JavaScript files the page loads, the ones in .next/static/chunks. Every compiled component checks its cache slots against a marker, Symbol.for("react.memo_cache_sentinel"), the value an empty slot holds. Each row is one recording and what its JavaScript held:
| Recording | Holds Symbol.for("react.memo_cache_sentinel")? | Compares a slot with it (===Symbol.for(…))? |
|---|---|---|
| No compiler | Yes. | No. |
reactCompiler: true | Yes. | Yes. |
React's own code holds the marker in both builds, because React fills new cache slots with it. Only the compiled build holds the compiler's check, ===Symbol.for("react.memo_cache_sentinel"). The names and the minified code around it belong to this build, so don't write tools that depend on them.
Opting out, and opting in
The compiler reads two directives, strings on their own as a function's first line: "use no memo" leaves that function as written, and "use memo" asks for it to be compiled. The React Compiler explains them and when to use each. What Next.js adds is the setting for annotation mode, reactCompiler: { compilationMode: "annotation" }, where only functions with "use memo" are compiled. Here is OrderForm asking for it on line 10:
"use client"; import { useState } from "react";import TeaList from "./tea-list";import { useRenderCount } from "./use-render-count"; const teas = ["Green tea", "Mint tea", "Lemon tea"]; export default function OrderForm() { "use memo"; const [note, setNote] = useState(""); const countRef = useRenderCount(); return ( <section> <label htmlFor="note">Note</label> <input id="note" value={note} onChange={(event) => setNote(event.target.value)} /> <p> OrderForm renders: <span ref={countRef} /> </p> <TeaList teas={teas} /> </section> );} Pick each version below and open step 2 in its Steps tab. Before you look, guess which versions keep TeaList at 1:
app/order-form.tsx"use client"; import { useState } from "react";import TeaList from "./tea-list";import { useRenderCount } from "./use-render-count"; const teas = ["Green tea", "Mint tea", "Lemon tea"]; export default function OrderForm() { const [note, setNote] = useState(""); const countRef = useRenderCount(); return ( <section> <label htmlFor="note">Note</label> <input id="note" value={note} onChange={(event) => setNote(event.target.value)} /> <p> OrderForm renders: <span ref={countRef} /> </p> <TeaList teas={teas} /> </section> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
OrderForm renders: 2
- Green tea
- Mint tea
- Lemon tea
TeaList renders: 2
- Focus
- the <input>
- Scrolled
- 0 px down
No requests during this step: the page already had what it needed
Nothing is compiled. Typing renders OrderForm, which makes a new <TeaList> element on line 19, so React calls TeaList again: step 2 shows TeaList renders: 2.
Each row is one version: TeaList's count after typing, and whether the page's JavaScript holds the compiler's cache checks.
| Version | TeaList renders after typing | Compiled code in the page's JavaScript? |
|---|---|---|
| Compiler off | 2 | No. |
reactCompiler: true | 1 | Yes. |
"use no memo" in OrderForm | 2 | Yes, for TeaList and the Hook. |
| Annotation mode, no directive | 2 | No. |
Annotation mode, "use memo" in OrderForm | 1 | Yes, for OrderForm. |
Look at the "use no memo" row. TeaList is still compiled, and still renders again. The memoization that saved TeaList was never inside TeaList: it was OrderForm keeping the element. Opt OrderForm out, and TeaList renders again on every key. The same goes the other way in annotation mode: asking for OrderForm alone was enough.
Checking the compiler's work
The compiler is quiet. When it can't compile a function, it leaves it as written and moves on, and next build passes either way. So check its work yourself. Each row is one way to check, what you see, and what it tells you:
| Where you look | What you see | What it tells you |
|---|---|---|
| React DevTools | A Memo ✨ badge next to each compiled component's name, in next dev. | Which components the compiler compiled. |
ESLint with eslint-config-next | Compiler rules like react-hooks/refs point at functions it skipped; general Hooks rules check correct React. | Why a function was left as written. |
| The built JavaScript | Compiled components compare cache slots with Symbol.for("react.memo_cache_sentinel"). | That the compiler ran in this build. |
| Your own measurements | Render counts, or the React DevTools profiler, before and after. | Whether the page got faster where it matters. |
React DevTools. The compiler runs in next dev too. Open your app in development with the React Developer Tools browser extension, open its Components tab, and look for a Memo ✨ badge next to each name: react.dev describes it as the sign that a component was compiled. (That's from React's documentation and from using it; the recordings here run next start without DevTools.)
ESLint. eslint-config-next, Next.js's ESLint config, turns on the recommended rules of eslint-plugin-react-hooks (version 7.1.1 with eslint-config-next 16.4.3). Since version 7, they include the compiler's own diagnostics, like react-hooks/refs, which point at functions the compiler skips because they break a rule it can see. The same preset also has the general Hooks rules, like react-hooks/exhaustive-deps, which check that your code is correct React whether or not the compiler is on; a report from one of those doesn't mean a component was skipped. Here is a TeaList that counts its renders in a ref while it renders, a broken rule on purpose:
app/tea-list.tsximport { useRef } from "react"; export default function TeaList({ teas }: { teas: string[] }) { const renders = useRef(0); renders.current += 1; return ( <section> <h2>Our teas</h2> <ul> {teas.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> <p>TeaList renders: {renders.current}</p> </section> );}$ npx eslint ./my-app/app/tea-list.tsx 5:3 error Error: Cannot access refs during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). /my-app/app/tea-list.tsx:5:3 3 | export default function TeaList({ teas }: { teas: string[] }) { 4 | const renders = useRef(0);> 5 | renders.current += 1; | ^^^^^^^^^^^^^^^ Cannot access ref value during render 6 | return ( 7 | <section> 8 | <h2>Our teas</h2> react-hooks/refs 5:3 error Error: Cannot access refs during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). /my-app/app/tea-list.tsx:5:3 3 | export default function TeaList({ teas }: { teas: string[] }) { 4 | const renders = useRef(0);> 5 | renders.current += 1; | ^^^^^^^^^^^^^^^ Cannot update ref during render 6 | return ( 7 | <section> 8 | <h2>Our teas</h2> react-hooks/refs 14:28 error Error: Cannot access refs during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). /my-app/app/tea-list.tsx:14:28 12 | ))} 13 | </ul>> 14 | <p>TeaList renders: {renders.current}</p> | ^^^^^^^^^^^^^^^ Cannot access ref value during render 15 | </section> 16 | ); 17 | } react-hooks/refs ✖ 3 problems (3 errors, 0 warnings) $ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentThe example adds eslint.config.mjs, and ESLint 9 with eslint-config-next in package.json, as create-next-app writes them when you pick ESLint. The Terminal tab shows npx eslint . first. Line 5 of app/tea-list.tsx changes renders.current while rendering, and line 14 reads it: Cannot access refs during render
, three errors from react-hooks/refs (two of them on line 5, one for reading and one for writing). Then next build passes: since Next.js 16 it doesn't run ESLint, and the compiler simply left this TeaList as written. Run ESLint in your editor and before every build, for example in CI. With the shop's own useRenderCount, which touches refs only in an Effect, ESLint printed nothing at all:
app/use-render-count.tsimport { useEffect, useRef } from "react"; // Counts the renders of the component that calls it. After each render,// it writes the count into the element that gets the returned ref.export function useRenderCount() { const elementRef = useRef<HTMLSpanElement>(null); const renders = useRef(0); useEffect(() => { renders.current += 1; if (elementRef.current) elementRef.current.textContent = String(renders.current); }); return elementRef;}$ npx eslint . $ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentThe built JavaScript. The table of what the page's JavaScript held is a search of the files in .next/static/chunks. A common tip says to search them for react/compiler-runtime, the module compiled code imports. In this production build, the text compiler-runtime wasn't there at all: the bundle didn't keep the import's name. The compiler's check was there, though how it looks is this build's detail, not something React promises. For a check you can rely on, use DevTools.
Measure. Fewer renders are only a win where renders were slow. Measure a slow interaction before and after with the React DevTools profiler (Profiling React apps), and watch how quickly the page answers clicks, its INP, as Core Web Vitals shows.
Turning it on in the tea shop
The compiler keeps only what didn't change. In this version, the teas live in OrderForm's state, and a button adds one:
app/order-form.tsx"use client"; import { useState } from "react";import TeaList from "./tea-list";import { useRenderCount } from "./use-render-count"; export default function OrderForm() { const [note, setNote] = useState(""); const [teas, setTeas] = useState(["Green tea", "Mint tea", "Lemon tea"]); const countRef = useRenderCount(); return ( <section> <label htmlFor="note">Note</label> <input id="note" value={note} onChange={(event) => setNote(event.target.value)} /> <button type="button" onClick={() => setTeas([...teas, "Oolong tea"])}> Add Oolong tea </button> <p> OrderForm renders: <span ref={countRef} /> </p> <TeaList teas={teas} /> </section> );}Each step was taken in real Chrome against next start. Pick one to see the page after it.
OrderForm renders: 3
- Green tea
- Mint tea
- Lemon tea
- Oolong tea
TeaList renders: 2
- Focus
- the button “Add Oolong tea”
- Scrolled
- 0 px down
No requests during this step: the page already had what it needed
Line 9 keeps the teas in state, and line 15's button gives setTeas a new array with Oolong tea at the end. In the Steps tab, step 2 is the note: TeaList renders: 1, as before. Step 3 is the click on Add Oolong tea: OrderForm renders: 3, TeaList renders: 2, and the list shows Oolong tea. teas was a new array, so the kept element was out of date and OrderForm made a new one. That's the whole deal: you write plain code, and the compiled OrderForm reuses the element it kept until teas, what it was made from, changes.
For an app that already works, The React Compiler walks through adopting it step by step: lint first, start with a few components, test and measure, then turn it on everywhere. In Next.js, those steps map to three settings you've now seen recorded: eslint-config-next for the lint, compilationMode: "annotation" with "use memo" to start small, and reactCompiler: true with an exact plugin version to finish.
Sort each piece of the shop's code: does Next.js compile it with reactCompiler: true, in the default mode?
OrderForm, in a file that starts with"use client", with state and JSX.TeaList, with no directive, imported only byOrderForm.useRenderCount, a Hook in a fileOrderFormimports.OrderPageinapp/page.tsx, a Server Component.- A date picker from a package in
node_modules. OrderFormwith"use no memo"as its first line.TeaListwithout"use memo", in annotation mode.formatPrice(price), a plain function that returns text.
Each card is one function in the tea shop. Sort it by what happens with reactCompiler: true, unless the card says annotation mode.
Common mix-ups
- "The compiler runs in the visitor's browser." No. It runs when Next.js builds your code, in
next buildand innext dev. The browser runs the code it wrote. - "Turning it on compiles my Server Components too." No. Next.js 16.4.3 gives the compiler only the code it builds for the browser. Server Components render once per request or build, so there's nothing to keep between renders.
- "
reactCompiler: trueis all it takes." No. Withoutbabel-plugin-react-compilerinstalled, the build stops withFailed to resolve package babel-plugin-react-compiler
. - "If the compiler can't handle a component, the build tells me." No. It leaves that component as written, and the build passes. ESLint's compiler rules, like
react-hooks/refs, point at it, andnext builddoesn't run ESLint. - "
"use no memo"in TeaList makes TeaList render on every key." No. The kept element lives inOrderForm, so React still skips TeaList. Opt out the parent to change that. - "Search the built files for
react/compiler-runtimeto check." Not in this production build: the module's name wasn't in the page's JavaScript. The compiler's check of its cache slots was.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
A teammate added reactCompiler: true and nothing else. Which package does the build say it couldn't find?
babel-plugin-react-compiler. reactCompiler: true asks Next.js to run it, and Next.js looks for it next to its own package. Install it with npm install --save-dev --save-exact babel-plugin-react-compiler.
Without the compiler, Chrome opens the page and types a note. What does TeaList renders say afterwards?
"use client";
import { useState } from "react";
import TeaList from "./tea-list";
import { useRenderCount } from "./use-render-count";
const teas = ["Green tea", "Mint tea", "Lemon tea"];
export default function OrderForm() {
const [note, setNote] = useState("");
const countRef = useRenderCount();
return (
<section>
<label htmlFor="note">Note</label>
<input id="note" value={note} onChange={(event) => setNote(event.target.value)} />
<p>
OrderForm renders: <span ref={countRef} />
</p>
<TeaList teas={teas} />
</section>
);
}
2. The page loaded with TeaList at 1. Typing rendered OrderForm, which made a new <TeaList> element, so React called TeaList again: the first recording's step 2 says TeaList renders: 2.
The compiler is on, and TeaList now starts with "use no memo" (line 4). After typing a note, what does TeaList renders say?
import { useRenderCount } from "./use-render-count";
export default function TeaList({ teas }: { teas: string[] }) {
"use no memo";
const countRef = useRenderCount();
return (
<section>
<h2>Our teas</h2>
<ul>
{teas.map((tea) => (
<li key={tea}>{tea}</li>
))}
</ul>
<p>
TeaList renders: <span ref={countRef} />
</p>
</section>
);
}
1. "use no memo" leaves TeaList as written, but OrderForm is still compiled and returns the element it kept, so React never calls TeaList again. The recording of this version says TeaList renders: 1.
The shop uses compilationMode: "annotation". You may add "use memo" to one component so that typing a note doesn't render TeaList. Which component?
OrderForm. It's the component that makes the <TeaList> element. Compiled, it keeps the element, and React skips TeaList: the annotation-mode recording with "use memo" in OrderForm says TeaList renders: 1.
The compiler is on. A teammate moved the teas array inside OrderForm (line 10). After typing a note, what does TeaList renders say?
"use client";
import { useState } from "react";
import TeaList from "./tea-list";
import { useRenderCount } from "./use-render-count";
export default function OrderForm() {
const [note, setNote] = useState("");
const countRef = useRenderCount();
const teas = ["Green tea", "Mint tea", "Lemon tea"];
return (
<section>
<label htmlFor="note">Note</label>
<input id="note" value={note} onChange={(event) => setNote(event.target.value)} />
<p>
OrderForm renders: <span ref={countRef} />
</p>
<TeaList teas={teas} />
</section>
);
}
1. The array on line 10 depends on nothing that changes, so the compiled OrderForm makes it once and keeps it, and keeps the <TeaList> element made from it. The recording of this version says TeaList renders: 1 after typing.
Which rule does ESLint report for this TeaList, and how do you fix it?
import { useRef } from "react";
export default function TeaList({ teas }: { teas: string[] }) {
const renders = useRef(0);
renders.current += 1;
return (
<section>
<h2>Our teas</h2>
<ul>
{teas.map((tea) => (
<li key={tea}>{tea}</li>
))}
</ul>
<p>TeaList renders: {renders.current}</p>
</section>
);
}
const countRef = useRenderCount();
// …
<p>
TeaList renders: <span ref={countRef} />
</p>react-hooks/refs: a ref is read and changed during render. Count in an Effect instead, as useRenderCount does, and show the count without reading the ref while rendering. With that version, ESLint printed nothing, and the compiler compiles TeaList.
Check your understanding
Question 1 of 7In Next.js 16, where do you turn the React Compiler on?
Choose an answer to see the explanation.
Question 2 of 7With
reactCompiler: true, Chrome typed a note into the order form. What doesTeaList renderssay afterwards?Choose an answer to see the explanation.
Question 3 of 7Which code does Next.js 16.4.3 give the React Compiler, with the default Babel plugin?
Choose an answer to see the explanation.
Question 4 of 7What does
TeaList renderssay after typing, in this version of OrderForm?Read the code, then predict"use client"; import { useState } from "react"; import TeaList from "./tea-list"; import { useRenderCount } from "./use-render-count"; const teas = ["Green tea", "Mint tea", "Lemon tea"]; export default function OrderForm() { "use no memo"; const [note, setNote] = useState(""); const countRef = useRenderCount(); return ( <section> <label htmlFor="note">Note</label> <input id="note" value={note} onChange={(event) => setNote(event.target.value)} /> <p> OrderForm renders: <span ref={countRef} /> </p> <TeaList teas={teas} /> </section> ); }Choose an answer to see the explanation.
Question 5 of 7In annotation mode, with no
"use memo"anywhere in the app, what does the compiler compile?Choose an answer to see the explanation.
Question 6 of 7ESLint reports
react-hooks/refsin this TeaList, and the build passes. What does the compiler do with TeaList?Read the code, then predictimport { useRef } from "react"; export default function TeaList({ teas }: { teas: string[] }) { const renders = useRef(0); renders.current += 1; return ( <section> <h2>Our teas</h2> <ul> {teas.map((tea) => ( <li key={tea}>{tea}</li> ))} </ul> <p>TeaList renders: {renders.current}</p> </section> ); }Choose an answer to see the explanation.
Question 7 of 7Does
next buildrun ESLint in Next.js 16?Choose an answer to see the explanation.
Key takeaways
- Install
babel-plugin-react-compilerand setreactCompiler: trueinnext.config.ts. It's stable since Next.js 16 (it wasexperimental.reactCompilerin 15) and off by default. - Next.js 16.4.3 runs it through Babel on Client Component code built for the browser, after a quick check of each file. Server Components, the server's copy of Client Components, and
node_modulesare left as written. - A compiled component still runs on every render, but reuses the values, functions and JSX elements it kept until what each was made from changes. A reused element lets React skip that child, with no
memo. "use no memo"opts one function out; annotation mode compiles only functions with"use memo". The memoization that skips a child lives in its parent.- The build passes even when the compiler skips code. Check with React DevTools'
Memo ✨badge, ESLint's compiler rules likereact-hooks/refs(run them yourself;next builddoesn't), and measurements.
Remember the one-liner.
One line in next.config.ts, and your Client Components keep what didn't change.
At release v16.4.3, the Turbopack rule that adds the compiler is in babel.rs, and the Babel loader's checks are in get-config.ts. How the React Compiler works, in the React course, reads the compiler itself.
Coming next: Accessibility. What Next.js does for screen reader and keyboard users when the page changes, and what's left for you to do.