cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

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.

By the end, you can
  • 01
    Turn the compiler onInstall its Babel plugin, set reactCompiler in next.config.ts, and know what it costs at build time.
  • 02
    Say what it compilesTell which code Next.js gives the compiler (Client Component code built for the browser) and what that code keeps between renders.
  • 03
    Opt 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.

The order page, without the compilerNext.js
app/
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>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Order tea
Oolong, please

OrderForm renders: 2

Our teas
  • 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:

The order page, with reactCompiler: trueNext.js
app/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */  cacheComponents: true,  partialPrefetching: true,  reactCompiler: true,}; export default nextConfig;
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Order tea
Oolong, please

OrderForm renders: 2

Our teas
  • 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).

Two columns, one for each recording, after typing a note. Left, without the compiler: you type, OrderForm renders, it returns a new TeaList element, and TeaList renders, so its count is 2. Right, with reactCompiler true: you type, OrderForm renders, it returns the TeaList element it kept from the first render, and React skips TeaList, so its count stays 1.Without the compilerYou type a noteOrderForm renders: 2a new <TeaList> elementTeaList renders: 2React calls TeaList againWith reactCompiler: trueYou type a noteOrderForm renders: 2the kept <TeaList> elementTeaList renders: 1React skips TeaList
The same keystrokes in both recordings; shaded cards are components React called. 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.
Real-life analogyA head chef, an editor, and the recipe books

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
The main idea

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:

Install the compiler's Babel pluginShell
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:

A new app with the compiler onShell
npx create-next-app@latest tea-shop --react-compiler

It 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:

The reactCompiler option
What you write in next.config.tsWhat it doesWhere it works
reactCompiler: trueCompiles 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 reactCompilerRuns 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:

reactCompiler: true, without the pluginNext.js
app/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */  cacheComponents: true,  partialPrefetching: true,  reactCompiler: true,}; export default nextConfig;
next build failed · Next.js 16.4.3
$ 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 frames

The 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:

The rule that adds Babel, from babel.rs at v16.4.3 (lines 123 to 131)Text
    // 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:

  1. Files in node_modules are skipped (get-config.ts, lines 119 to 126), and so is any build for the server.
  2. A quick text check: in the default mode, the file must hold "use memo", a name like useState (use then a capital letter), </ or /> (babel.rs, lines 245 to 257). A file of plain helpers never pays for Babel.
  3. 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).
An editor window for the app folder lists layout.tsx, page.tsx, order-form.tsx, tea-list.tsx and use-render-count.ts. next build makes two copies. The browser's copy holds order-form.tsx, tea-list.tsx and use-render-count.ts, shaded: compiled. The server's copy holds layout.tsx, page.tsx, order-form.tsx and tea-list.tsx, not shaded: left as written.applayout.tsxpage.tsxorder-form.tsxtea-list.tsxuse-render-count.tsfor the browserorder-form.tsxtea-list.tsxuse-render-count.tscompiled: a memo cache in each componentfor the serverlayout.tsxpage.tsxorder-form.tsxtea-list.tsxleft as written: they render once
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:

What the page's JavaScript held
RecordingHolds Symbol.for("react.memo_cache_sentinel")?Compares a slot with it (===Symbol.for(…))?
No compilerYes.No.
reactCompiler: trueYes.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:

app/order-form.tsx, opted in with "use memo"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() {  "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:

Playground: which version keeps TeaList from rendering again?
The order page, with this versionNext.js
app/
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>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Order tea
Oolong, please

OrderForm renders: 2

Our teas
  • 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

Try it yourself
No compiler

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.

Every version was built with next build from Next.js 16.4.3, run with next start, and typed into in real Chrome: open /, then type Oolong, please into Note. Only next.config.ts, package.json or the first line of OrderForm changes. Reset goes back to the first version.

Each row is one version: TeaList's count after typing, and whether the page's JavaScript holds the compiler's cache checks.

What each version did
VersionTeaList renders after typingCompiled code in the page's JavaScript?
Compiler off2No.
reactCompiler: true1Yes.
"use no memo" in OrderForm2Yes, for TeaList and the Hook.
Annotation mode, no directive2No.
Annotation mode, "use memo" in OrderForm1Yes, 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:

Ways to check the compiler
Where you lookWhat you seeWhat it tells you
React DevToolsA Memo ✨ badge next to each compiled component's name, in next dev.Which components the compiler compiled.
ESLint with eslint-config-nextCompiler 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 JavaScriptCompiled components compare cache slots with Symbol.for("react.memo_cache_sentinel").That the compiler ran in this build.
Your own measurementsRender 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:

ESLint, then next build, on a TeaList that breaks a ruleNext.js
app/
app/tea-list.tsx
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>  );}
Recorded from Next.js 16.4.3
$ 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 content

The 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:

ESLint on the shop with useRenderCountNext.js
app/
app/use-render-count.ts
import { 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;}
Recorded from Next.js 16.4.3
$ 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 content

The 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:

Typing a note, then adding a tea, with the compiler onNext.js
app/
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>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Order tea
Oolong, pleaseAdd Oolong tea

OrderForm renders: 3

Our teas
  • 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?

Compiled, or left as written?
  • OrderForm, in a file that starts with "use client", with state and JSX.
  • TeaList, with no directive, imported only by OrderForm.
  • useRenderCount, a Hook in a file OrderForm imports.
  • OrderPage in app/page.tsx, a Server Component.
  • A date picker from a package in node_modules.
  • OrderForm with "use no memo" as its first line.
  • TeaList without "use memo", in annotation mode.
  • formatPrice(price), a plain function that returns text.
Try it yourself
0 of 8 correct

Each card is one function in the tea shop. Sort it by what happens with reactCompiler: true, unless the card says annotation mode.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "The compiler runs in the visitor's browser." No. It runs when Next.js builds your code, in next build and in next 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: true is all it takes." No. Without babel-plugin-react-compiler installed, the build stops with Failed 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, and next build doesn't run ESLint.
  • ""use no memo" in TeaList makes TeaList render on every key." No. The kept element lives in OrderForm, so React still skips TeaList. Opt out the parent to change that.
  • "Search the built files for react/compiler-runtime to 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.

Exercise 1 · Warm-upRead the failed build

A teammate added reactCompiler: true and nothing else. Which package does the build say it couldn't find?

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upCount without the compiler

    Without the compiler, Chrome opens the page and types a note. What does TeaList renders say afterwards?

    Starter codeTSX
    "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>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeOpt out the child

      The compiler is on, and TeaList now starts with "use no memo" (line 4). After typing a note, what does TeaList renders say?

      Starter codeTSX
      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>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeChoose what to annotate

        The shop uses compilationMode: "annotation". You may add "use memo" to one component so that typing a note doesn't render TeaList. Which component?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeTeas made on every render

          The compiler is on. A teammate moved the teas array inside OrderForm (line 10). After typing a note, what does TeaList renders say?

          Starter codeTSX
          "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>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFix what ESLint reports

            Which rule does ESLint report for this TeaList, and how do you fix it?

            Starter codeTSX
            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>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The React Compiler in Next.js quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In Next.js 16, where do you turn the React Compiler on?

                Choose an answer to see the explanation.

              2. Question 2 of 7With reactCompiler: true, Chrome typed a note into the order form. What does TeaList renders say afterwards?

                Choose an answer to see the explanation.

              3. 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.

              4. Question 4 of 7What does TeaList renders say after typing, in this version of OrderForm?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                "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.

              5. 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.

              6. Question 6 of 7ESLint reports react-hooks/refs in this TeaList, and the build passes. What does the compiler do with TeaList?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                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>
                  );
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7Does next build run ESLint in Next.js 16?

                Choose an answer to see the explanation.

              Key takeaways

              • Install babel-plugin-react-compiler and set reactCompiler: true in next.config.ts. It's stable since Next.js 16 (it was experimental.reactCompiler in 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_modules are 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 like react-hooks/refs (run them yourself; next build doesn't), and measurements.

              Remember the one-liner.
              One line in next.config.ts, and your Client Components keep what didn't change.

              Read it in Next.js's own code

              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.

              CompleteFrontend Clear concepts. Working examples.