00 Explorable explanation · the missing piece, made mechanical
One <Counter/> becomes a registered reference, a manifest row, a Flight tuple, a required module — and only then a live, clickable button.
"React records a reference" is not the explanation — it is the part that still needs explaining. The real bridge is a chain of concrete artifacts: a "use client" source file becomes a build-time client reference proxy, recorded in a client reference manifest; during the RSC render React Flight resolves that reference into module metadata — a module id, chunk filenames, and an export name; the metadata rides the RSC payload to the browser, where the Flight client decoder preloads the chunk, calls __webpack_require__ by id, reads the export, and hydrates the DOM the browser already painted. Press play and ride the whole line, station by station.
Press play to ride the line from source file to hydration
Cyan stations happen at build time. Amber stations happen on the server during the request. Violet stations happen in the browser. Watch the same component change form at every stop.
__webpack_require__ with the exact module id, and read the exact export. The bridge is plumbing, not magic.
01 Two related server-side products
one render · two outputsThe server does two things, not one
App Router rendering splits into two related server-side products from a single render. React renders Server Components into the RSC / Flight payload; then Next uses that payload plus Client Component JavaScript instructions to render HTML. On the client, the HTML is the immediate preview, the RSC payload reconciles the server and client trees, and JavaScript hydrates the Client Components. Press play to watch one render fan out into both.
Press play to split one render into two products
The RSC pass answers "what is the tree, where are the client boundaries, what props cross?" The HTML pass answers "what can the browser paint right now?"
What is the component tree result? Where are the Client Component boundaries? What props cross from server to client? Which client modules must the browser know about?
What can the browser paint immediately? It uses the RSC payload plus Client Component JS instructions to produce first-frame markup — even for the client boundaries.
So a Client Component wears a different hat in each phase. During the RSC render it is a reference / boundary. During the HTML render it may be prerendered for initial markup. During hydration it becomes a real browser module loaded from JS chunks. Same component, three jobs — which is exactly the next idea.
02 The correct mental model
module graph first · render tree second"use client" marks a module boundary, not a visual subtree
The phrase "use client" does not first mark a region of the screen. It introduces a server–client boundary in the module dependency tree and creates a subtree of client modules. During render the framework server-renders the root and walks the render tree, opting out of evaluating any code imported from client-marked code. Press play to peel the one render tree apart into two module graphs.
Press play to separate graph from tree
The render tree stays one logical UI. The module graph splits in two. The server never calls Counter() as ordinary code — that boundary becomes a client reference proxy.
// stays on the server
app/page.tsx
├─ db queries
├─ server-only utilities
├─ layout.tsx
└─ non-"use client" components// crosses to the browser app/Counter.tsx "use client" ├─ react useState ├─ browser event handlers └─ modules imported by Counter.tsx
<Page> contains <main> contains <h1>Dashboard</h1> and <Counter/>. The server walks that tree, renders the server parts, and at the <Counter/> boundary it does not evaluate the client module — it records a reference instead.
03 The signature idea
one symbol · three identitiesCounter has three identities at once
This is the whole bridge in one frame. The symbol you wrote — <Counter/> — exists as three different things, and the manifest plus Flight metadata are the threads that connect them. Press play to watch each identity appear.
Press play to reveal the three identities
The source module is the file you wrote. The server-side reference is a registered proxy that only knows "a client component lives here." The browser module is the real JS chunk that runs.
app/Counter.tsx — the actual implementation you authored, marked "use client".ClientReference: "this client component exists at this resource key + export." It throws if you call it on the server.04 The simple case, made precise
no client boundary · output ships, code does notA Server Component can produce HTML without ever shipping JS
"Server Components don't run in the browser" does not mean "Server Components don't produce HTML." It means the browser sees their output, not their implementation. With no client boundary in sight, the pipeline is short — and nothing about Page is ever sent to the browser.
export default function Page() { return ( <main> <h1>Hello from the server</h1> <p>This is a Server Component.</p> </main> ) }
<!-- result, not implementation --> <main> <h1>Hello from the server</h1> <p>This is a Server Component.</p> </main> // no Page.js · no hydration · 0 KB
async function Page() — it receives the <main> the function returned.
05 The demystification layer · build time
6.1 + 6.2 · one source becomes two build artifactsBuild time writes a proxy and a manifest
When a Server Component imports a Client Component, the build does not use the original client implementation as ordinary server code. The next-flight-loader emits a registered reference, and a manifest plugin records where the real module lives. Press play to watch app/Counter.tsx become both.
Press play to compile the client boundary
The server-side value is not the real component. It is a registered reference with enough identity for React Flight to serialize it — plus a manifest row mapping that identity to a module id and chunks.
import { registerClientReference } from "react-server-dom-webpack/server" export default registerClientReference( function () { throw new Error("Attempted to call the default export from the server...") }, "/app/Counter.tsx", // resource key "default" // export name )
So <Counter initialCount={0} /> becomes, during the RSC render, an element whose type is ClientReference("/app/Counter.tsx", "default") with props: { initialCount: 0 }. The throwing function body is the point: a client export can only be rendered as a Component or passed as props to one — never invoked as server code.
type ClientReferenceManifestEntry = { id: string | number // e.g. 4872 name: string // e.g. "default" / "*" chunks: Array<string> // chunk ids + filenames async?: boolean }
server/app/[route]_client-reference-manifest.js, which assign into globalThis.__RSC_MANIFEST[route]. That is server/build metadata for the rendering pipeline — not a public browser API you read by hand. Don't memorize the exact keys (clientModules, ssrModuleMapping, …); memorize the shape: source file + export → module id → chunk filenames → export name → async marker.
06 The demystification layer · server render
6.3 · resolveClientReferenceMetadataThe render turns a reference into a loading tuple
When React Flight serializes a client reference, it must turn /app/Counter.tsx + default into concrete loading metadata: a module id, chunk files, an export name, and an async marker. React's server-side webpack Flight config has resolveClientReferenceMetadata — it takes the manifest plus the reference, looks up the reference's $$id, and returns [id, chunks, name]. Press play to follow the lookup.
Press play to resolve the reference
The reference's $$id is the lookup key. The manifest is the table. The tuple is the answer, and it gets encoded into the Flight stream at the boundary's position, alongside the serialized props.
// the resolved Flight metadata for Counter [ 4872, ["123", "static/chunks/app/counter-abc123.js"], "default" ] // async modules carry a 4th element: [ 4872, [ …chunks… ], "default", 1 ]
4872 is the module id, the strings are the chunks to load, "default" is the export to read. This tuple is the thing that travels.
07 The demystification layer · browser
6.4 · preload → require → read → hydrateThe browser decodes the tuple and loads the real module
On the browser side, React's Flight client receives metadata for a client reference. It can use the already-resolved metadata directly: resolveClientReference returns [id, chunks, name], preloadModule loads the chunks, and requireModule calls __webpack_require__(metadata[ID]) before returning the requested export. The client never inspects the DOM and never reads the manifest file — it acts on metadata that arrived in the stream. Press play.
Press play to load and hydrate Counter
Decode the metadata, preload the chunk, require the module by id, read the export — and only then does the painted button become a live React component with working useState and onClick.
// Flight says module id: 4872 chunks: ["123", "static/chunks/app/counter-abc123.js"] export: "default" // React Flight client preloadModule(metadata) → loadChunk("123", "static/chunks/app/counter-abc123.js") requireModule(metadata) → __webpack_require__(4872) → moduleExports["default"] → the actual Counter function
08 The transport, in one minute
self.__next_f · the queue, not the resolverself.__next_f.push(...) is the courier
On initial load the browser gets one text/html response, and Next embeds the Flight data as inline scripts. The first push seeds a bootstrap segment into self.__next_f; later chunks push Flight data. The array buffers segments until Next's app runtime loads, drains the backlog, and patches .push so future chunks pipe straight into the Flight reader. The metadata tuple from the last section rides inside these chunks — the array carries it, React's decoder resolves it.
Press play to watch the bytes arrive and become a component
One HTTP response carries both the painted HTML and the Flight data as inline scripts. The array buffers the segments until the runtime drains them into the Flight reader, then hands the model to hydrateRoot.
zoom in · what actually rides inside one chunk
A Flight chunk is just newline-delimited rows
Each row is id:tag JSON. The I row carries the client-reference tuple [id, chunks, name]; the model points at it with a lazy $L reference the decoder resolves on demand — preload the chunk, require the module, swap in the real component.
rsc: 1 request header — no HTML document is transferred. The transport changes; the resolution machinery (metadata → preload → require → read) stays exactly the same.
09 The cast of the bridge
tap any piece to open its roleEvery piece, and the one job it does
The bridge is not one mechanism — it is a relay of small, boring, specific artifacts, each with a single responsibility. Tap a card to see what it is, why it matters, and how it looks under the hood.
10 What the bridge is not
three correctionsCommon confusions, corrected
Most misunderstandings of RSC come from collapsing two of the three identities, or from imagining the browser does something clever it never does. Tap each to see the correction.
11 The shortest correct version
The whole bridge, in one paragraph
In an RSC app, Server Components execute on the server and produce a rendered RSC result. When the render reaches a Client Component, it does not execute that client module as ordinary server-owned code — the build replaced that import with a registered client reference / proxy. React Flight serializes that reference using the client reference manifest, turning the source module/export into a module id, chunk filenames, export name, and props. Next streams the resulting Flight payload to the browser, often through inline self.__next_f.push(...) scripts during the initial HTML response. The client runtime converts those pushed chunks into a readable Flight stream, React decodes the module metadata, preloads the referenced chunks, requires the module by id, reads the export — and then React DOM hydrates the existing HTML.