Server Rendering — Fizz, Flight, and React Server Components
Prerequisites
- ›Articles 1-5 (full client-side React understanding)
- ›Basic understanding of HTTP streaming and chunked transfer encoding
- ›Familiarity with React Server Components concepts (use client, use server directives)
- ›Basic webpack or bundler knowledge for the integration sections
Server Rendering — Fizz, Flight, and React Server Components
React's server story has evolved from a simple renderToString into three distinct architectures, each solving a different problem. Fizz streams HTML with Suspense support. Flight Server serializes React Server Components into a streaming wire protocol. Flight Client deserializes that protocol back into React elements on the client. Together, they enable the RSC model where some components run only on the server, some only on the client, and the framework orchestrates the boundary.
In this final article, we'll survey all three architectures, examine how hooks work differently on the server, trace the Flight serialization protocol, and understand how bundler integrations (webpack, turbopack) handle 'use client' and 'use server' directives.
The Three Server Architectures at a Glance
graph TD
subgraph "Server"
Fizz["Fizz<br/>(react-server/ReactFizzServer)<br/>Renders components → HTML stream"]
Flight["Flight Server<br/>(react-server/ReactFlightServer)<br/>Renders RSC → JSON-like stream"]
end
subgraph "Client"
FlightClient["Flight Client<br/>(react-client/ReactFlightClient)<br/>Deserializes RSC stream → React elements"]
Reconciler["Fiber Reconciler<br/>(react-reconciler)<br/>Reconciles elements → DOM"]
end
Fizz -->|"HTML stream"| Browser["Browser DOM"]
Flight -->|"RSC payload"| FlightClient
FlightClient -->|"React elements"| Reconciler
Reconciler --> Browser
Fizz is traditional SSR with superpowers. It renders your React tree to an HTML stream, but unlike the legacy renderToString, it handles Suspense boundaries — emitting fallback HTML immediately, then streaming the resolved content when data arrives, along with inline <script> tags that swap the content in-place.
Flight Server doesn't produce HTML. It renders Server Components and serializes their output into a streaming protocol where client component references remain as opaque references (not rendered on the server). The output is a sequence of typed chunks — React elements, client references, serialized props, Suspense boundaries.
Flight Client receives this stream and reconstructs React elements, resolving client component references to actual component functions via the bundler manifest. The result feeds directly into the regular Fiber reconciler.
Fizz — Streaming Server-Side Rendering
Fizz has its own work loop, independent of the Fiber reconciler. The core is in ReactFizzServer.js — at over 5,000 lines, it's a complete rendering engine.
The performWork function drives Fizz's rendering:
export function performWork(request: Request): void {
if (request.status === CLOSED || request.status === CLOSING) {
return;
}
const prevContext = getActiveContext();
const prevDispatcher = ReactSharedInternals.H;
// ... install Fizz's hooks dispatcher, then render
}
Fizz processes the component tree synchronously within each work chunk, generating HTML chunks as it goes. When it hits a Suspense boundary, it has two strategies:
- If the content is ready: render it inline, no boundary overhead
- If the content suspends: emit the fallback HTML in-place, continue rendering. When the data resolves, stream the completed HTML plus an inline
<script>that swaps the fallback for the real content
This out-of-order streaming is what makes Fizz powerful — the browser starts painting immediately with fallbacks, then progressively enhances as data arrives.
The DOM-specific Fizz entry is ReactDOMFizzServerBrowser.js:
function renderToReadableStream(
children: ReactNodeList,
options?: Options,
): Promise<ReactDOMServerReadableStream> {
return new Promise((resolve, reject) => {
// ... creates Fizz request, starts work, returns ReadableStream
});
}
Tip: Fizz and the Fiber reconciler are completely separate rendering engines. Fizz doesn't create fibers, doesn't use the work loop from Article 3, and doesn't use the lane system. It has its own task model, its own context stack, and its own hooks dispatcher. They share the
react-serverpackage only because both need to render components.
Server-Side Hooks — A Different Dispatcher
As we learned in Article 4, hooks work through the dispatcher pattern. On the server, different dispatchers are installed that implement a subset of the hook API.
Fizz's hooks are in ReactFizzHooks.js. Flight's hooks are in ReactFlightHooks.js.
| Hook | Fizz (SSR) | Flight (RSC) |
|---|---|---|
useState |
✅ Works (initial state only, no updates) | ❌ Not available |
useReducer |
✅ Works (initial state only) | ❌ Not available |
useEffect |
⏭️ No-op (effects are client-only) | ❌ Not available |
useLayoutEffect |
⏭️ No-op (warns in dev) | ❌ Not available |
useRef |
✅ Works (returns {current}) | ❌ Not available |
useMemo |
✅ Works | ✅ Works |
useCallback |
✅ Works | ✅ Works |
useContext |
✅ Works | ✅ Works |
useId |
✅ Works (generates deterministic IDs) | ✅ Works |
use |
✅ Works (can await promises) | ✅ Works |
The ReactServer.js entry point exports only the hooks available in server contexts — notably missing useState, useEffect, useReducer, and other client-state hooks:
export {
Children, Activity, Fragment, Profiler, StrictMode,
Suspense, ViewTransition,
cloneElement, createElement, createRef, use,
forwardRef, isValidElement, lazy, memo,
cache, cacheSignal,
useId, useCallback, useDebugValue, useMemo,
version, captureOwnerStack,
};
This is enforced by the react-server export condition in package.json — when a bundler resolves react on the server, it gets this restricted API surface.
Flight Server — Serializing React Server Components
The Flight Server renders Server Components and produces a streaming protocol. The key insight is what it does when it encounters a client component (one marked with 'use client'):
sequenceDiagram
participant FS as Flight Server
participant SC as Server Component
participant CC as Client Component Reference
FS->>SC: Render <ServerComponent />
SC-->>FS: Returns <div><ClientComponent name="Alice" /></div>
FS->>FS: Serialize <div> as React element
FS->>CC: Encounter ClientComponent
Note over FS,CC: Don't render! Serialize as reference
FS->>FS: Emit: {type: "client-ref", id: "module123", props: {name: "Alice"}}
Server Components are fully rendered on the server — their function bodies execute, their useMemo/useCallback/useContext hooks run. The output is serialized. But client components are not rendered — they become opaque references in the stream, including their props (which must be serializable).
The wire protocol is a series of newline-delimited chunks, each with a type tag and an ID. Chunks reference each other by ID, building up a tree structure incrementally. Suspense boundaries create natural chunk boundaries — when a Server Component suspends, the Flight Server can emit a "pending" chunk and later replace it with the resolved content.
Props passed from Server Components to Client Components must be serializable via React's serialization format — which supports JSON primitives, React elements, client references, server references, Promises, and other structured types. Functions (other than Server Actions) cannot be passed as props across the boundary.
Flight Client — Deserializing the RSC Stream
The Flight Client receives the streaming protocol and reconstructs React elements. It processes chunks as they arrive, resolving references and building up the component tree.
flowchart TD
Stream["RSC Stream<br/>(chunks arriving over time)"]
Parse["Parse chunk type + ID"]
Parse -->|"Element chunk"| RE["Create React element"]
Parse -->|"Client reference"| CR["Resolve via bundler manifest<br/>requireModule(moduleId)"]
Parse -->|"Pending chunk"| SP["Create Suspense-wrapped promise"]
Parse -->|"Resolved chunk"| RP["Resolve pending promise<br/>Trigger re-render"]
RE --> Tree["React Element Tree"]
CR --> Tree
SP --> Tree
RP --> Tree
Tree --> Reconciler["Feed to Fiber Reconciler"]
When the Flight Client encounters a client reference chunk, it uses the bundler's module manifest to resolve the reference to an actual component function. This is where the bundler integration becomes critical — the server and client must agree on module IDs.
Pending chunks integrate with Suspense. When the Flight Client encounters a chunk that references an unresolved promise, it creates a thenable that the reconciler can suspend on. When the Flight Server later streams the resolved chunk, the promise resolves, triggering a re-render that picks up the new content.
The result of Flight Client processing is a regular React element tree — indistinguishable from what you'd get from local JSX. This tree feeds directly into the Fiber reconciler (the same work loop from Article 3), which reconciles it against the current DOM.
Bundler Integration — Webpack and Turbopack
The RSC model requires deep bundler integration. The 'use client' and 'use server' directives are transformed by bundler plugins into reference objects that the Flight protocol can serialize.
React provides bundler-specific packages:
react-server-dom-webpack— for webpackreact-server-dom-turbopack— for turbopack (Next.js)react-server-dom-parcel— for Parcelreact-server-dom-esm— for native ESM
Each package provides both a server and client entry. The server entry for webpack wires up the Flight Server with webpack-specific module resolution, while the client entry wires up the Flight Client with webpack's chunk loading.
graph TD
subgraph "Build Time"
UC["'use client' directive"] --> BP["Bundler Plugin"]
US["'use server' directive"] --> BP
BP --> CM["Client Manifest<br/>(moduleId → chunk mapping)"]
BP --> SM["Server Manifest<br/>(actionId → handler mapping)"]
end
subgraph "Runtime (Server)"
FSR["Flight Server"] --> CM
FSR -->|"serializes client refs"| Stream["RSC Stream"]
end
subgraph "Runtime (Client)"
Stream --> FCL["Flight Client"]
FCL --> CM2["Client Manifest"]
CM2 -->|"resolves refs to modules"| Components["Loaded Components"]
end
A client reference is an object like { $$typeof: REACT_CLIENT_REFERENCE, $$id: "app/Button.js#default" }. When the Flight Server encounters this as a component type, it doesn't try to render it — it serializes the reference. On the client, the Flight Client resolves $$id through the bundler manifest to find the webpack chunk and export name.
A server reference works in the opposite direction. When a Server Action is passed as a prop to a client component, it becomes a reference that the client can call back to — typically via an HTTP POST to the server, where the reference is resolved to the original function.
Tip: The RSC architecture means the same
reactpackage is used on both server and client, but with different export conditions. On the server,import React from 'react'resolves toReactServer.js(via thereact-servercondition), which exports only server-compatible APIs. This prevents you from accidentally usinguseStatein a Server Component — the export simply doesn't exist.
The Complete Picture
Across these six articles, we've traced React's architecture from its monorepo structure and build system, through the Fiber data structure that represents your components, the work loop that processes them, the hooks system that gives them state, the host config that bridges to the DOM, and finally the server architectures that render your components before they reach the browser.
graph TD
subgraph "Your Code"
JSX["JSX Components"]
end
subgraph "react package"
API["Public API<br/>createElement, hooks stubs"]
SI["SharedInternals.H<br/>(dispatcher bridge)"]
end
subgraph "react-reconciler"
Fiber["Fiber Tree<br/>(double-buffered)"]
WL["Work Loop<br/>(beginWork ↓ completeWork ↑)"]
Hooks["Hook Implementations<br/>(linked list on fiber)"]
Lanes["Lane Model<br/>(31-bit priority)"]
Commit["Commit Phase<br/>(3 sub-phases)"]
end
subgraph "react-dom-bindings"
HC["Host Config<br/>(createInstance, commitUpdate)"]
Events["Event System<br/>(delegation, SyntheticEvent)"]
end
subgraph "react-server"
Fizz["Fizz SSR"]
Flight["Flight RSC"]
end
JSX --> API
API --> SI
SI --> Hooks
Hooks --> Fiber
WL --> Fiber
Lanes --> WL
WL --> Commit
Commit --> HC
HC --> Events
JSX --> Fizz
JSX --> Flight
Every piece connects: the fork system enables the build pipeline to produce environment-specific bundles; the Fiber data structure carries state through renders; the lane model prioritizes work; the work loop processes fibers efficiently; hooks store state on fibers via the dispatcher bridge; and the host config translates abstract operations into real DOM mutations.
React's architecture is a case study in separation of concerns at an industrial scale. The reconciler doesn't know about the DOM. The react package doesn't know about the reconciler. The Scheduler doesn't know about React. Each piece communicates through narrow, well-defined interfaces — and the fork system wires them together at build time rather than runtime, eliminating abstraction overhead in production.