Back to Blog

React Flight Protocol: A Deep Technical Dive

Understanding the Wire Protocol Powering React Server Components


Table of Contents

  1. Introduction
  2. The Problem Space
  3. What is React Flight?
  4. Architecture Overview
  5. The Wire Format
  6. Serialization Deep Dive
  7. Streaming Mechanics
  8. Client-Side Reconstruction
  9. Reference Types and Chunks
  10. Practical Implementation
  11. Performance Characteristics
  12. Comparison with Alternatives
  13. Debugging Flight Payloads
  14. Conclusion

Introduction

React Flight Protocol is the serialization and streaming protocol that enables React Server Components (RSC) to function. While RSC gets the spotlight, Flight is the unsung hero—a carefully designed wire protocol that solves the non-trivial problem of streaming a component tree from server to client while maintaining React's component model semantics.

This article dissects Flight at the byte level, examining how it serializes React elements, handles references, streams data progressively, and reconstructs the virtual DOM on the client.


The Problem Space

Traditional Server Rendering Limitations

Traditional SSR (Server-Side Rendering) has a fundamental constraint: it produces HTML strings.

┌─────────────────────────────────────────────────────────┐
│                    Traditional SSR                       │
├─────────────────────────────────────────────────────────┤
│  Server                           Client                 │
│  ┌─────────┐                     ┌─────────┐            │
│  │ React   │ ──── HTML ────────► │ Parse   │            │
│  │ Render  │      String         │ HTML    │            │
│  └─────────┘                     └────┬────┘            │
│                                       │                  │
│                                       ▼                  │
│                                  ┌─────────┐            │
│                                  │ Hydrate │            │
│                                  │ (Re-run │            │
│                                  │  all JS)│            │
│                                  └─────────┘            │
└─────────────────────────────────────────────────────────┘

Problems:

  1. Hydration requires re-executing all component logic on the client
  2. Bundle size bloat—all components ship to the client
  3. No streaming of component structure—only HTML chunks
  4. Loss of component boundaries in the HTML output
  5. Data fetching logic duplicated between server and client

What We Actually Need

┌─────────────────────────────────────────────────────────┐
│                    Ideal Model                           │
├─────────────────────────────────────────────────────────┤
│  • Stream component tree structure (not just HTML)       │
│  • Preserve component boundaries and semantics           │
│  • Reference client components without shipping code     │
│  • Stream async data as it resolves                      │
│  • Enable partial hydration / selective interactivity    │
└─────────────────────────────────────────────────────────┘

This is precisely what React Flight Protocol provides.


What is React Flight?

React Flight is a streaming serialization protocol designed specifically for React's component model. It consists of two complementary packages:

PackagePurpose
react-server-dom-webpack/serverServer-side serialization (Flight Server)
react-server-dom-webpack/clientClient-side deserialization (Flight Client)

Note: The webpack suffix indicates bundler integration. Alternatives exist for other bundlers (Turbopack, Vite, etc.).

Core Responsibilities

┌──────────────────────────────────────────────────────────────┐
│                    Flight Server                              │
├──────────────────────────────────────────────────────────────┤
│  1. Serialize React element trees to streamable format        │
│  2. Handle async components (Suspense boundaries)             │
│  3. Resolve Promises and stream results incrementally         │
│  4. Encode references to Client Components                    │
│  5. Serialize supported data types (Date, Map, Set, etc.)     │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│                    Flight Client                              │
├──────────────────────────────────────────────────────────────┤
│  1. Parse incoming Flight stream                              │
│  2. Reconstruct React element tree                            │
│  3. Resolve Client Component references to actual components  │
│  4. Handle streaming updates (Suspense resolution)            │
│  5. Integrate with React's reconciler                         │
└──────────────────────────────────────────────────────────────┘

Architecture Overview

Request/Response Flow

┌─────────────────────────────────────────────────────────────────────┐
│                         Flight Architecture                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│   Browser                          Server                             │
│   ┌─────────┐                     ┌─────────────────────────────┐   │
│   │         │ ── RSC Request ───► │  Server Component Tree       │   │
│   │         │                     │  ┌─────────────────────┐     │   │
│   │         │                     │  │ <App>               │     │   │
│   │ Flight  │                     │  │   <Layout>          │     │   │
│   │ Client  │                     │  │     <ServerComp/>   │     │   │
│   │         │                     │  │     <ClientComp/>───┼──┐  │   │
│   │         │                     │  │   </Layout>         │  │  │   │
│   │         │                     │  │ </App>              │  │  │   │
│   │         │                     │  └─────────────────────┘  │  │   │
│   │         │                     │           │               │  │   │
│   │         │                     │           ▼               │  │   │
│   │         │                     │  ┌─────────────────────┐  │  │   │
│   │         │                     │  │   Flight Server     │  │  │   │
│   │         │                     │  │   renderToPipe-     │  │  │   │
│   │         │                     │  │   ableStream()      │  │  │   │
│   │         │                     │  └──────────┬──────────┘  │  │   │
│   │         │                     │             │             │  │   │
│   │         │ ◄── Flight Stream ──┼─────────────┘             │  │   │
│   │         │    (Chunked JSON)   │                           │  │   │
│   │         │                     │  Client Component         │  │   │
│   │         │ ◄── JS Bundle ──────┼── Reference Map ◄─────────┘  │   │
│   └─────────┘                     └─────────────────────────────┘   │
│                                                                       │
└─────────────────────────────────────────────────────────────────────┘

Component Classification

Flight differentiates between two component types:

// Server Component (default in App Router)
// - Runs ONLY on server
// - Can access databases, filesystems, secrets
// - Serialized to Flight format
// - Never shipped to client bundle

async function ServerComponent() {
  const data = await db.query('SELECT * FROM posts');
  return <div>{data.map(post => <Post key={post.id} {...post} />)}</div>;
}

// Client Component (marked with 'use client')
// - Runs on client (and server for SSR)
// - Has access to hooks, browser APIs, interactivity
// - Referenced by module ID in Flight payload
// - Shipped in client bundle

'use client';
function ClientComponent({ onClick }) {
  const [state, setState] = useState(0);
  return <button onClick={onClick}>{state}</button>;
}

The Wire Format

Flight uses a line-delimited JSON streaming format. Each line is a self-contained chunk that can be parsed independently.

Basic Structure

ROW_ID:TYPE_TAG JSON_PAYLOAD\n
ComponentDescription
ROW_IDUnique identifier for this chunk (hexadecimal)
TYPE_TAGSingle character indicating payload type
JSON_PAYLOADThe actual data (JSON-encoded)
\nNewline delimiter

Type Tags

TagNameDescription
(none)ModelReact element tree or data model
IModule ImportClient Component reference
HHintPreload hints (fonts, scripts)
EErrorError boundary information
TTextRaw text content
PPromisePending async value
SSymbolReact Symbol (Suspense, Fragment)
BBlobBinary data reference

Real-World Payload Example

Consider this component tree:

// Server Component
async function Page() {
  const user = await getUser();
  return (
    <main>
      <h1>Welcome, {user.name}</h1>
      <ClientCounter initial={user.visits} />
    </main>
  );
}

The Flight payload might look like:

0:I["(app-client)/./src/components/Counter.js",["default"],"ClientCounter"]
1:{"name":"John","visits":42}
2:["$","main",null,{"children":[["$","h1",null,{"children":["Welcome, ","$1.name"]}],["$","$L0",null,{"initial":"$1.visits"}]]}]

Let's decode this:

Line 0: Client Component Import

I["(app-client)/./src/components/Counter.js", ["default"], "ClientCounter"]
  • Module path: ./src/components/Counter.js
  • Export: default
  • Display name: ClientCounter

Line 1: Data Model

{"name": "John", "visits": 42}
  • User data fetched on server

Line 2: React Element Tree

["$", "main", null, {"children": [...]}]
  • $ prefix indicates React element
  • $L0 references Client Component from line 0
  • $1.name references data from line 1

Serialization Deep Dive

Element Encoding

React elements are encoded as tuples:

// React Element
<div className="container" id="main">Hello</div>

// Flight Encoding
["$", "div", null, {"className": "container", "id": "main", "children": "Hello"}]
//  │    │     │     └── Props object
//  │    │     └── Key (null if none)
//  │    └── Element type
//  └── Element marker

Special References

Flight uses sigil prefixes for special values:

PrefixMeaningExample
$React Element["$", "div", ...]
$LLazy (Client Component)"$L0" → reference line 0
$FServer Reference (Action)"$F1" → server function
$DDate"$D2024-01-15T..."
$nBigInt"$n12345678901234567890"
$uundefined"$u"
$-0Negative zero"$-0"
$-InfinityNegative infinity"$-Infinity"
$NaNNaN"$NaN"

Supported Data Types

// Flight can serialize these types natively:

const supportedTypes = {
  // Primitives
  string: "hello",
  number: 42,
  boolean: true,
  null: null,
  undefined: undefined,      // → "$u"
  bigint: 123n,              // → "$n123"

  // Objects
  plainObject: { a: 1 },
  array: [1, 2, 3],

  // Built-in Objects
  date: new Date(),          // → "$D2024-..."
  map: new Map(),            // → Special encoding
  set: new Set(),            // → Special encoding

  // Binary
  arrayBuffer: new ArrayBuffer(8),
  typedArrays: new Uint8Array([1,2,3]),

  // React-specific
  reactElement: <div />,     // → ["$", "div", ...]
  promise: Promise.resolve() // → Streamed separately
};

Circular Reference Handling

Flight handles object references to avoid duplication:

const shared = { data: "reused" };
const tree = {
  a: shared,
  b: shared  // Same reference
};

// Flight output uses references:
// 0:{"data":"reused"}
// 1:{"a":"$0","b":"$0"}
//         │       └── Reference to line 0
//         └── Reference to line 0

Streaming Mechanics

Progressive Rendering

Flight's killer feature is progressive streaming. Async boundaries don't block the entire response.

async function Page() {
  return (
    <main>
      <Header />           {/* Renders immediately */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />  {/* Streams when ready */}
      </Suspense>
      <Footer />           {/* Renders immediately */}
    </main>
  );
}

Stream Timeline

Time ─────────────────────────────────────────────────────────►

T=0ms   Server starts rendering
        ┌──────────────────────────────────────┐
        │ 0:["$","main",null,{                 │
        │   "children":[                       │
        │     ["$","$L1",null,{}],  // Header  │
        │     ["$","$Suspense",null,{          │
        │       "fallback":"$3",               │
        │       "children":"$P4"  // Pending   │
        │     }],                              │
        │     ["$","$L2",null,{}]   // Footer  │
        │   ]                                  │
        │ }]                                   │
        └──────────────────────────────────────┘
                    │
                    ▼ (streamed to client)

T=50ms  Client renders Header, Skeleton, Footer

T=800ms SlowComponent resolves
        ┌──────────────────────────────────────┐
        │ 4:["$","div",null,{                  │
        │   "children":"Slow data loaded!"     │
        │ }]                                   │
        └──────────────────────────────────────┘
                    │
                    ▼ (streamed to client)

T=850ms Client replaces Skeleton with SlowComponent

Server Implementation

import { renderToPipeableStream } from 'react-server-dom-webpack/server';

function handler(req, res) {
  const { pipe } = renderToPipeableStream(
    <App />,
    bundlerConfig,
    {
      onError(error) {
        console.error(error);
      },
    }
  );

  res.setHeader('Content-Type', 'text/x-component');
  res.setHeader('Transfer-Encoding', 'chunked');
  pipe(res);
}

Chunked Transfer Encoding

Flight leverages HTTP chunked transfer encoding:

HTTP/1.1 200 OK
Content-Type: text/x-component
Transfer-Encoding: chunked

2f
0:["$","div",null,{"children":"Hello"}]

2a
1:["$","span",null,{"children":"World"}]

0

Each hex number indicates the chunk byte length, enabling progressive parsing.


Client-Side Reconstruction

Parsing the Stream

import { createFromFetch } from 'react-server-dom-webpack/client';

async function fetchServerComponent(url) {
  const response = fetch(url);

  // createFromFetch returns a Promise<ReactElement>
  // that resolves progressively as chunks arrive
  const root = await createFromFetch(response);

  return root;
}

// In your React tree:
function App() {
  const [tree, setTree] = useState(null);

  useEffect(() => {
    fetchServerComponent('/rsc/page')
      .then(setTree);
  }, []);

  return tree;
}

Module Resolution

When Flight Client encounters a Client Component reference:

// Flight payload
'0:I["./Counter.js",["default"],"Counter"]'

// Client resolution process:
// 1. Parse module reference
const moduleRef = {
  id: "./Counter.js",
  exports: ["default"],
  name: "Counter"
};

// 2. Look up in webpack manifest
const chunkId = webpackManifest["./Counter.js"]["default"];

// 3. Ensure chunk is loaded
await __webpack_chunk_load__(chunkId);

// 4. Get actual component
const Counter = __webpack_require__(moduleRef.id)[moduleRef.exports[0]];

// 5. Use in tree
<Counter {...props} />

Suspense Integration

Flight integrates deeply with React Suspense:

// Simplified Flight Client internals
class FlightClient {
  chunks = new Map();

  parseChunk(line) {
    const [id, data] = parseLine(line);

    if (this.chunks.has(id)) {
      // Resolve pending promise
      const { resolve } = this.chunks.get(id);
      resolve(data);
    } else {
      // Store for later reference
      this.chunks.set(id, { value: data, status: 'resolved' });
    }
  }

  getChunk(id) {
    if (!this.chunks.has(id)) {
      // Create pending promise for Suspense
      let resolve;
      const promise = new Promise(r => resolve = r);
      this.chunks.set(id, { promise, resolve, status: 'pending' });
      throw promise; // Suspense will catch this
    }

    const chunk = this.chunks.get(id);
    if (chunk.status === 'pending') throw chunk.promise;
    return chunk.value;
  }
}

Reference Types and Chunks

Client Reference

Marks a component that should run on the client:

// Server sees:
import ClientComp from './ClientComp'; // 'use client' module

// Flight serializes as:
{
  $$typeof: Symbol.for('react.client.reference'),
  $$id: './ClientComp.js#default',
  $$async: false
}

// Wire format:
'0:I["./ClientComp.js",["default"],"ClientComp"]'

Server Reference

Marks a function that runs on the server (Server Actions):

// Server:
async function submitForm(formData) {
  'use server';
  await db.insert(formData);
}

// Flight serializes as:
{
  $$typeof: Symbol.for('react.server.reference'),
  $$id: 'abc123',
  $$bound: null
}

// Wire format:
'0:F["abc123"]'

// Client can call this:
// POST /_rsc?actionId=abc123

Bound Server References

Server Actions can have pre-bound arguments:

// Server:
async function deletePost(postId, formData) {
  'use server';
  await db.delete('posts', postId);
}

// When passed to client:
<form action={deletePost.bind(null, post.id)}>

// Flight encodes bound arguments:
'0:F["deletePost",["post-123"]]'
//                  └── Bound argument

Practical Implementation

Custom Flight Server

// flight-server.js
import { renderToPipeableStream } from 'react-server-dom-webpack/server';
import { createServer } from 'http';
import { createElement } from 'react';

// Bundle manifest from webpack
import manifest from './dist/client-manifest.json';

const server = createServer(async (req, res) => {
  if (req.url.startsWith('/_rsc')) {
    // Dynamic import of server component
    const { default: Page } = await import('./app/page.js');

    const { pipe } = renderToPipeableStream(
      createElement(Page, { url: req.url }),
      manifest,
      {
        onError(err) {
          console.error('Flight error:', err);
          res.statusCode = 500;
        },
      }
    );

    res.setHeader('Content-Type', 'text/x-component');
    pipe(res);
  }
});

server.listen(3000);

Custom Flight Client

// flight-client.js
import { createFromFetch } from 'react-server-dom-webpack/client';
import { use, Suspense, startTransition } from 'react';

// Cache for navigation
const cache = new Map();

function fetchRSC(url) {
  if (!cache.has(url)) {
    cache.set(url, createFromFetch(
      fetch(`/_rsc${url}`, {
        headers: { 'Accept': 'text/x-component' }
      })
    ));
  }
  return cache.get(url);
}

function ServerRoot({ url }) {
  // `use` hook unwraps the promise
  const content = use(fetchRSC(url));
  return content;
}

function App() {
  const [url, setUrl] = useState(location.pathname);

  const navigate = (newUrl) => {
    startTransition(() => {
      setUrl(newUrl);
    });
  };

  return (
    <NavigationContext.Provider value={navigate}>
      <Suspense fallback={<Loading />}>
        <ServerRoot url={url} />
      </Suspense>
    </NavigationContext.Provider>
  );
}

Webpack Configuration

// webpack.config.js
const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');

module.exports = {
  // Client bundle config
  entry: './src/client.js',
  plugins: [
    new ReactServerWebpackPlugin({
      isServer: false,
    }),
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'react-server-dom-webpack/loader',
          options: { emit: 'client' },
        },
      },
    ],
  },
};

Performance Characteristics

Payload Size Comparison

┌─────────────────────────────────────────────────────────────┐
│              Payload Size: Flight vs Alternatives           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Traditional SSR (HTML + Hydration Data):                   │
│  ████████████████████████████████████████████  ~45KB        │
│                                                              │
│  JSON API + Client Rendering:                                │
│  ██████████████████████████████  ~30KB (data + JS)          │
│                                                              │
│  Flight Protocol:                                            │
│  ████████████████  ~16KB (no duplicate data)                │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Time to First Byte (TTFB)

Flight streams immediately without waiting for async operations:

Traditional SSR:
[Wait for all data] ──────────────► [Send HTML]
        800ms                            50ms
                                   TTFB: 850ms

Flight Streaming:
[Start stream immediately] ──► [Stream chunks as ready]
        10ms                          840ms (progressive)
                              TTFB: 10ms

Bundle Impact

// Components NOT in client bundle:
// - ServerComponent.js    ✗ Not shipped
// - DatabaseQuery.js      ✗ Not shipped
// - ServerUtils.js        ✗ Not shipped

// Components IN client bundle:
// - ClientComponent.js    ✓ Shipped
// - InteractiveForm.js    ✓ Shipped

// Result: Potentially 30-50% smaller client bundles

Comparison with Alternatives

Flight vs GraphQL

AspectFlightGraphQL
SchemaComponent tree (implicit)Explicit type definitions
Query LanguageN/A (server determines)GraphQL queries
StreamingNative, per-component@stream/@defer extensions
Client CodeAutomatic (React)Manual resolver mapping
CachingResponse-levelField-level normalization
Use CaseReact UI renderingGeneral data fetching

Flight vs tRPC

AspectFlighttRPC
ProtocolCustom streamingJSON-RPC over HTTP
Type SafetyVia JSX typesEnd-to-end TypeScript
OutputReact elementsRaw data
FrameworkReact onlyFramework agnostic
StreamingBuilt-inVia SSE or WebSocket

Flight vs Remix/Qwik

AspectFlightRemixQwik
StreamingComponent-levelRoute-levelComponent-level
ResumabilityNo (needs JS)NoYes
BundlePartialFull routeLazy per-interaction
Learning CurveHighMediumHigh

Debugging Flight Payloads

Chrome DevTools

// In Network tab, look for requests with:
// - Content-Type: text/x-component
// - Look at Response tab for raw Flight payload

// Pretty-print Flight payload:
function parseFlightPayload(text) {
  return text.split('\n')
    .filter(Boolean)
    .map(line => {
      const colonIndex = line.indexOf(':');
      const id = line.slice(0, colonIndex);
      const payload = line.slice(colonIndex + 1);
      return {
        id,
        type: getTypeFromPayload(payload),
        data: JSON.parse(payload.replace(/^\w/, ''))
      };
    });
}

Logging Middleware

// Debug middleware for Flight
function flightDebugMiddleware(req, res, next) {
  const originalWrite = res.write;

  res.write = function(chunk) {
    console.log('[Flight Chunk]', chunk.toString());
    return originalWrite.apply(this, arguments);
  };

  next();
}

Common Issues

// Issue: "Objects are not valid as a React child"
// Cause: Trying to serialize non-serializable value
// Fix: Ensure all props are serializable

// Issue: "Could not find the module"
// Cause: Client component not in manifest
// Fix: Rebuild with proper webpack config

// Issue: Infinite loading
// Cause: Circular Promise references
// Fix: Check for circular async dependencies

Conclusion

React Flight Protocol represents a paradigm shift in how we think about server-client communication in React applications. By serializing the component tree itself rather than raw data, Flight enables:

  1. True Streaming — Components render as their data becomes available
  2. Zero-Bundle Server Components — Server code never ships to client
  3. Automatic Code Splitting — Client components load on demand
  4. Seamless Data Colocation — Fetch where you render
  5. Type-Safe Boundaries — Props are validated at serialization

Key Takeaways

┌──────────────────────────────────────────────────────────────┐
│  Flight Protocol Mental Model                                 │
├──────────────────────────────────────────────────────────────┤
│                                                               │
│  1. It's a SERIALIZATION format, not a transport protocol    │
│                                                               │
│  2. It preserves COMPONENT SEMANTICS, not just data          │
│                                                               │
│  3. It STREAMS progressively, chunks are independent         │
│                                                               │
│  4. Client Components are REFERENCES, not inline code        │
│                                                               │
│  5. It integrates with SUSPENSE for async boundaries         │
│                                                               │
└──────────────────────────────────────────────────────────────┘

Flight isn't just an implementation detail—it's the architectural foundation that makes React Server Components possible. Understanding it deeply enables better debugging, performance optimization, and appreciation for the engineering that powers modern React applications.


Further Reading


Published: 2024 | Last Updated: 2024 Author: Technical Deep Dive Series

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.