Every step from browser request to interactive app — the pipeline, the payload, the dual renderers, and the APIs that wire it all together.
Three distinct phases. Two renderers. One continuous pipeline. Each phase hands off to the next — the output of one is the input of the next.
await db.query(), file
reads, env secrets. Its implementation never ships to the browser; only its rendered
output and references do.$L client references. Server Functions / Actions become
opaque $F server references.$L1 and $L2
resolve through the bundler into the JavaScript chunks needed for hydration.One request starts one server render. That render gives the browser two things it needs: HTML for what should be painted now, and Flight data for the React tree, props, suspended regions, and client references that describe that UI. They can stream at overlapping times, but they are not unrelated outputs and they are not mirror copies of each other.
$L references — they do not execute during the RSC render.
They may still be pre-rendered later by the HTML render, and they run in the browser during hydration.$RC script as that boundary resolves.
<script> chunks
(e.g. self.__next_f), interleaved with the markup — so React can hydrate
Client Component boundaries and reuse the server tree description.
$RC) —
not interactive yet. Once the client bundle loads, React re-reads the inlined Flight
(createFromReadableStream), $L references import their chunks, and
event listeners attach in place. → TTI.The overview above moves fast, and a few steps quietly assume things that, left unsaid, leave you guessing — where Server Components show up in HTML, what Flight is for, how a reference turns into an import, and what hydration actually replays. Here's the precise version of each.
No. Server Components run on the server and their returned JSX is part of the server-rendered result. React DOM Server uses that renderable result to emit HTML, so normal Server Component output can absolutely become visible markup. What does not ship is the Server Component's JavaScript implementation. Client Components are different: they appear as boundaries, may get initial server-rendered HTML, and later hydrate in the browser.
Yes — there are two related server-side concerns. The RSC/server layer executes Server Components and produces the Flight model: rendered output, serialized props, suspended regions, and client references. The HTML layer uses the renderable result to stream markup. React's word Client can be confusing here: it often means "a consumer of the server output," so the browser is a client, and the server HTML renderer is also consuming the RSC result.
The "Two Formats" picture is useful — but mechanically this is a pipeline, not two independent renderers printing from the same blueprint.
They overlap in meaning, but they answer different questions. HTML answers: "what should the browser paint now?" The Flight payload answers: "what React tree, props, suspended regions, and client references describe this UI?" The browser cannot reconstruct that React model from HTML alone, so React keeps the Flight data for hydration and later client-side navigations.
$L lazy reference actually become an import()?The $L2 sitting in the tree is a pointer to a row the renderer
emitted right beside it. That row — 2:I[…] — is the bundler's client-manifest
entry: the module's path, the chunk file(s) to fetch, and the export name. When React
resolves the reference — lazily, only when that node is about to render — it hands the
descriptor to the bundler runtime, which loads the chunk and returns the real component.
$L = lazy; the I row = import this module.
No. Hydration is about wiring up interactivity — not replaying the whole tree.
"use client" mean "this code runs only in the browser"?No — it's a boundary marker, not a runtime switch. It marks the entry point into the client module graph, so the bundler ships that module and emits a manifest entry for it. The code still runs on the server once during SSR to produce HTML, then again in the browser to hydrate. "Client" only distinguishes it from a Server Component — it does not mean the code skips the server.
No — they're separate concerns, and most apps use both. SSR is the HTML renderer: it turns the renderable React result into markup for the first paint. RSC decides which components run only on the server and describes their result as Flight data. Together, they give you server-rendered HTML for visible UI and no browser bundle for Server Component implementations.
The Flight renderer serializes the Server Component result. It runs a real React
server render — calling Server Components, awaiting async work, and recording the result as the
Flight wire format: a stream of numbered rows. Plain serializable values are inlined;
values that need special treatment get type-prefixed references. To encode a "use client"
component it consults the bundler's client manifest — that's how the component
becomes an I row (path + chunk + export) pointed at by $L. React DOM Server
is the HTML side: it turns the renderable result into markup.
createFromReadableStream come from, and how does it get into the browser?It's the client half of the very same Flight package —
createFromReadableStream from react-server-dom-<bundler>/client.browser
(there are .edge and .node builds too, which the SSR pass uses). Your
framework's client entry point imports it, so the bundler ships it inside
your client JS bundle — that's literally how it reaches the browser. The Flight
payload doesn't need a second request on first load: the framework usually inlines it into
the HTML as <script> chunks (Next pushes onto
self.__next_f; other frameworks use their own bootstrap shape). For later
client-side navigations, the browser fetch()es a fresh Flight stream and pipes
response.body straight in.
createFromReadableStream actually work?It's a thin wrapper over the Flight client. Given the Flight ReadableStream,
it reads the rows incrementally and returns a Thenable — a
Promise-like value that resolves to the root React element tree. As chunks arrive it fills a
row-keyed model: $L references resolve to client modules (loaded through the bundler
runtime), $@ references resolve when their promise's row streams in, plain rows inline
directly. Because the result is a Thenable, you hand it straight to React's use() /
Suspense — so the tree can render progressively as it streams, instead of blocking
on the whole payload. Server and client are mirror images:
JSON.stringify the tree?Because plain JSON can't carry what a React tree needs.
JSON.stringify(tree).Set/Map/Date/BigInt, or a
reference to a client module or another row. Flight adds $-prefixed types
and a numbered-row stream so values can resolve over time — which is exactly
what makes Suspense streaming possible.$F) become a function the client can call — and POST back?Same trick as a Client Component, run in reverse. At build time each "use server"
function gets a hashed action id and is registered in a server map. On the
client build the function is replaced by a tiny proxy —
createServerReference(id, callServer) — so what the Client Component actually receives is
a callable stub. In the payload it travels as a server reference:
{$$typeof: Symbol.for("react.server.reference"), $$id: "…"} (the $F row).
Calling the stub runs callServer(id, args): it encodeReply(args) (the same
rich serializer the payload uses) and POSTs to the server with the action id in a
header. The server looks the id up, decodeReplys the args, runs the function, and streams
back a fresh Flight payload — the re-rendered tree plus the return value —
decoded with the same createFromReadableStream. (With <form action={fn}>
it degrades gracefully: no JS → the browser just POSTs the form, handled server-side by
decodeAction.)
"use client" scan actually produce?Two things, in two passes. When a Server Component imports a "use client"
module, the server build does not include that component's code — it
replaces it with a client reference:
{$$typeof: Symbol.for("react.client.reference"), $$id: "<module-id>"} (via
registerClientReference). That's exactly why calling a Client Component from the server
throws — there's nothing to call, only a reference. The client build does a
separate pass that compiles the real component against the browser runtime and
code-splits it into a chunk. The piece that ties them together is the
client manifest the bundler emits — a map from each reference id to its file:
The directive also marks that module and all its transitive imports as client code — and the real module is compiled for the browser bundle. During the RSC render, the server sees a reference; during the HTML render, the component may be pre-rendered to inert markup.
react package become two different runtimes?Through Node/bundler export conditions. React's package.json
ships a react-server entry in its exports map. Code resolved under that
condition — the bundler's "react-server" layer, or Node's --conditions react-server —
gets a different build of React: one with the Server Component machinery and
without client-only APIs like useState / useEffect. Everything
else (Client Components, the SSR pass, the browser) resolves the normal build.
react.import { useState } from "react" resolves to a
hook-less build in the server layer and the full build everywhere else — same
specifier, two runtimes. That's why a hook in a Server Component is a build-time
error, not a runtime one.A boundary that isn't ready doesn't block the response — it ships a placeholder now and patches in the real content later, on both streams in parallel.
HTML side (Fizz): the server flushes the shell with the fallback inside
boundary markers — <!--$?--> (pending) … <!--/$--> plus a
<template id="B:0"> anchor. When the data resolves it streams the real HTML into a
hidden div, then an inline script calls React's $RC ("complete boundary") to swap the
fallback for the content. React/frameworks may batch or schedule reveals so a burst of resolutions
does not flash jarringly.
Flight side: the pending child is a promise; the serializer
(serializeThenable) emits it as a $@ reference and keeps the stream open.
When it resolves, a later row with that id streams in and the client swaps the fallback.
<script>self.__next_f.push(…)</script> tags in the page source — what are they?They're how the App Router inlines the Flight payload into the first HTML document. The same Flight bytes React needs to hydrate ride along inside the text/html response, so the first load needs no second request for the payload — paintable markup and hydration data arrive in one stream.
The shape is a self-flushing queue — the same trick analytics snippets use for dataLayer.push. One early script creates the global array; every chunk after it is just a push, and because each inline <script> runs the instant the browser parses it, the chunks append in order as the document streams:
Each push is a two-tuple [tag, data]. The tag is a small integer: 0 bootstraps the queue (no data); 1 — by far the most common — carries a string of Flight rows, the exact newline-separated numbered-row grammar from Q9 (0:, 2:I[…], $L, $@…). So the shell can flush before a slow Suspense boundary's rows even exist; they stream in as later push calls.
When the client bundle boots it upgrades the array: it replaces push with a function that feeds each chunk into an in-memory ReadableStream, which createFromReadableStream reads. Pushes that landed before the JS ran are already sitting in the array and get drained on init — so delivery never waits for the bundle, and the bundle never misses an early chunk. The same render, two transports for the same Flight:
Content-Security-Policy: inline pushes need 'unsafe-inline' or a per-request nonce.No new HTML document. The router issues an RSC request for the
destination (Next adds a ?_rsc=… query and an Rsc: 1 header) and the server
returns a Flight payload, not a page. Two things keep it cheap:
Partial rendering — only the route segments that change are
re-rendered and sent; shared layouts above the change are preserved.
/dashboard/settings → /dashboard/analytics re-renders only the leaf; the
dashboard layout and the root <html> stay put.
Router cache — an in-memory client cache keyed by segment holds the
payloads of visited and prefetched routes, so back/forward and revisits are instant.
<Link> prefetches a route's payload as it scrolls into view.
The new Flight tree is reconciled into the existing one, so Client Component state outside the changed segment survives the navigation. This is the concrete form of "re-rendering a Server Component = asking the server again" from Q6.
Real. The Flight format is not just inert display data: decoding it resolves references and reconstructs values, so deserializing untrusted Flight-shaped input is a security-sensitive operation. The important direction is the server decoding the client's input — a Server Function / Action call POSTs an encoded body that the server decodes before running the referenced server code.
Takeaway: keep React, your RSC framework, and your bundler integration patched according to their advisories. Treat every Server Function / Action argument as untrusted input: validate it, authorize it, and never assume that a client-provided payload is safe just because it targets server code.
The same pipeline as an explorable. A single token travels four swim-lanes — Server, the two Renderers, the Network, and the Browser. Scrub it, step it, or click any node to crack open the real React internals and the Flight protocol behind that step.
Press play to trace the request
Each frame highlights as the request travels its lane. Blue and purple lanes are server-only — that code never ships. The cyan lane is the one HTTP response; the green lane is the browser turning bytes into pixels and then into an interactive app.
Every frame above is choreographed by a handful of pieces inside the React runtime. Click any card for the real APIs, the props rules, and the wire format it owns.
Watch the data travel — from the SQL query to the browser screen — one step at a time, in the exact order each stage depends on the last. Press play, or step through it.
One server render produces two useful forms. The HTML stream is the paintable form: what the browser can show immediately. The Flight payload is the React data form: the Server Component result, serialized props, suspended regions, and Client Component references React needs for hydration and navigation. Why this is a pipeline, not two unrelated prints →
Produces HTML — visual markup for the browser to paint. Server Component output is included; Client Component boundaries may receive initial markup before they hydrate.
Produces Flight payload — structured React tree data. Client Components become module references here, even if their initial HTML appears in the HTML stream.
A shell is the HTML React can emit immediately — the page structure, with holes where something isn't ready. A hole waits on either data (a Suspense boundary) or JavaScript (a Client Component) — and each kind is filled by a completely different mechanism. Step through one response and watch both.
$RCThe shell ships a fallback inside the hole. When the data resolves on the server, React streams the real markup as a <div hidden> plus a one-line script that relocates it into the slot. This runs before hydration — no component JavaScript needed to fill the content.
A Client Component renders to static HTML — a shell that looks right but has no handlers. hydrateRoot walks that existing DOM, loads the component's chunk, and attaches listeners in place. The shell wakes up without being rebuilt.
Each line in the Flight payload has a row ID, a type prefix, and a content body. The type prefix tells the React Flight client how to interpret that row in this simplified example.
"$" = React
element. Intrinsic HTML tags are inlined as strings. DB results (title, comments) are fully
resolved JSON props. Client Component slots are $L1, $L2 — lazy references to be loaded.
import("chunk-abc123") when it
encounters $L1.
onLike(42) fires a POST to /_next/action with this action ID. The actual SQL never ships
to the browser.
$L5)
streams in as a later chunk in the same HTTP response — no second round trip. The fallback slot
is replaced in-stream.
After FCP, the async JS loads. React walks the server-rendered DOM and the client-side React model simultaneously — attaching handlers without touching the DOM.
{()=>setOpen(!open)} attached to existing <button> node.$F3 wired to callServer handler.The "use client" directive creates a serialization boundary. Props must be serializable — they are serialized through the Flight payload. Ordinary functions cannot cross. Server Functions / Actions cross only as opaque server references, not as function source code or closures.
"use client" boundary.Every API involved in the RSC pipeline — from server-side streaming to client-side hydration to the module boundary directives.
onShellReady when above-the-fold content is ready — enabling fast FCP
without waiting for all data.renderToPipeableStream. Used in
edge runtimes (Vercel Edge, Cloudflare Workers). Returns a ReadableStream compatible
with the Fetch API Response.ReactDOM.hydrate().ReadableStream and reconstructs the
React element tree. Resolves $L lazy references via dynamic import().
Returns a thenable React tree.createFromReadableStream that accepts
a fetch() Promise directly. Used during client-side RSC navigation to consume the new
route's Flight payload response.callServer callback registered in createFromReadableStream,
which POSTs to the server action endpoint.I{} Client Reference row for this module instead of inlining it. The
module's code ships to the client bundle.F{} row — an opaque callable reference. The function's implementation stays on the
server. Client receives only the action ID to POST to.