Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fused renderer - #2

Open
switz wants to merge 22 commits into
mainfrom
fused-renderer
Open

Fused renderer#2
switz wants to merge 22 commits into
mainfrom
fused-renderer

Conversation

@switz

@switzswitz commented Mar 25, 2026

Copy link
Copy Markdown
Owner

Full report here: react#36143

switzand others added 22 commits March 25, 2026 08:34
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
TIM-471: Fizz internals analysis for fused renderer
)
* TIM-471: Fizz internals analysis for fused renderer
Comprehensive analysis of ReactFizzServer.js covering:
- renderElement() dispatch table and insertion points
- Request object shape and lifecycle
- Task/Segment model and how they interact
- Suspense boundary suspend → resolve → stream flow
- The hydration data TODO at line 5944 (exact insertion point)
- How components are called via renderWithHooks
- Concrete entry points for fused renderer changes
* TIM-473: Client-side hydration markers and DOM walking analysis
Comprehensive analysis covering:
- All HTML comment markers emitted by Fizz (Suspense, Activity, FormState)
- hydrateRoot() DOM walking algorithm and state machine
- Suspense boundary hydration flow (dehydrated fragments, selective hydration)
- getNextHydratable() behavior with unknown markers (silently skipped)
- Proposed client boundary marker format (<!--C:ID--> / <!--/C-->)
- Where markers plug into server (FizzConfigDOM) and client (FiberConfigDOM)
- Reconciler changes (HydrationContext, BeginWork)
- How to skip server-only DOM during hydration
- Progressive/selective hydration compatibility
react#4)
Comprehensive feasibility analysis comparing three approaches:
- Approach A: Reimplement Flight logic in Fizz (fragile, 900 lines to maintain)
- Approach B: Flight as a library (extractable for detection, not for serialization)
- Approach B+: Extract pure detection functions + new minimal props serializer
Key findings:
- isClientReference and resolveClientReferenceMetadata are pure functions
with zero renderer state dependencies — trivially callable from Fizz
- renderModelDestructive is NOT extractable (deeply coupled to Request/Task)
- But we don't need it — client boundary props are a narrow subset
- A focused ~150 line serializer handles the common case
- Narrowing scope to sync server components first delivers most perf benefit
Includes:
- 10-item assumption inventory with stability ratings
- Evolving feature impact assessment (View Transitions, Fragment Refs, etc.)
- Sentinel test strategy for automated breakage detection
- Progressive fallback path for unsupported cases
- Recommended task scope adjustments
)
Add performance validation spike measuring the three-pass SSR pipeline:
Flight serialize → Flight deserialize → Fizz HTML render.
Key findings:
- Flight overhead is 54-79% of total SSR time across all scenarios
- Large e-commerce page (226 products): 1.89ms of 3.51ms is Flight overhead
- Flight wire format is 55-68% of total bytes (pure intermediate waste)
- Projected 2x throughput improvement from fusion
Deliverables:
- design/fused-renderer-perf-validation.md: full analysis with data
- scripts/bench/fused-renderer-bench.js: profiling harness
- scripts/bench/fused-renderer-scenarios.js: test app scenarios
…act#7)
* TIM-472: Flight wire format and client boundary detection analysis
Comprehensive analysis covering:
- isClientReference() detection via Symbol.for('react.client.reference')
- ClientReference shape (99689typeof, 99689id, 99689async)
- Flight wire format: row format, chunk types (Model, Import, Error, etc.)
- Import chunk metadata: module ID, chunks array, export name
- Props serialization at client boundaries
- Async server component handling (thenable → ping → retry)
- Module reference resolution (build-time proxies → manifest → metadata)
- Implications for fused renderer: detection, extraction, manifest access
* TIM-482: go/no-go checkpoint — GO with narrow scope (Approach B+)
Decision: proceed with fused renderer using Approach B+ (Flight as library
+ focused props serializer), phased rollout starting with sync server
component execution in Fizz.
Key evidence:
- Flight overhead is 54-79% of total SSR time (TIM-483)
- Detection functions are trivially extractable (TIM-481)
- Hydration markers are backwards-compatible (TIM-473)
- Projected 2x throughput improvement
Phased approach:
- Phase 1: TIM-474, TIM-475 (server-side fusion)
- Phase 2: TIM-476, TIM-477 (client hydration optimization)
- Phase 3: TIM-478-480 (coexistence, benchmarks, edge cases)
Each phase is a decision gate.
Replaces the synthetic v1 benchmark (TIM-483) with realistic scenarios:
- Async server components with simulated DB/cache fetches (1-20ms)
- Realistic prop sizes (blog posts ~10KB, products ~3KB each)
- 4-8 Suspense boundaries per scenario
- Isolated phase measurement to separate data fetch from serialization
Key finding: pure serialization overhead is 1-4% of total SSR time
(0.3-0.5ms out of 14-23ms). Data fetching dominates at 87-90%.
The fused renderer would yield only 2-4% throughput improvement,
which does not justify its engineering complexity.
Recommendation: do not proceed with fused renderer.
v2 measured wall-clock time with simulated I/O, which hid the CPU cost.
Throughput is limited by CPU, not I/O. Under concurrent load on a single
Node.js thread:
c=25, 226 products:
- Fizz only: 526 req/s, p99=49ms, heap +72MB
- Full pipeline: 102 req/s, p99=342ms, heap +282MB
- Drop: 5.2x throughput, 7.0x p99, +210MB heap
The overhead worsens under load (4x at c=1 → 5.9x at c=50) because
the 349KB wire format buffer per request creates GC pressure that
compounds with concurrency.
This directly explains the 400→40 rps drop in real deployments:
React-level 3-6x × framework overhead 1.5-2x ≈ 10x.
Recommendation: proceed with fused renderer.
…n Fizz (react#10)
Add fusedMode + bundlerConfig fields to Fizz's Request object, threaded
through from all DOM server entry points (Node, Edge, Browser, Bun) via
experimental_fusedMode and experimental_bundlerConfig options.
In renderElement(), when fusedMode is true:
- Client references (19013typeof === Symbol.for('react.client.reference'))
are detected and currently fall through to renderFunctionComponent.
TIM-475 will add hydration marker emission for these.
- Server components (functions without client reference tag) render via
the existing renderFunctionComponent/renderWithHooks path, which
already handles sync and async (via Suspense) components correctly.
The CLIENT_REFERENCE_TAG constant is defined locally in ReactFizzServer.js
using Symbol.for() rather than importing from Flight, keeping Fizz
decoupled from Flight internals per Approach B+.
Tests cover: sync server components, nested components, props passing,
async components via Suspense, streaming, null/fragment returns, and
client reference fallthrough.
…eact#11)
When fusedMode is true and Fizz encounters a client component
(type.20854typeof === CLIENT_REFERENCE_TAG), it now:
1. Emits <!--C:ID--> / <!--/C--> comment markers around the component's
HTML output in the segment chunks
2. Renders the component to HTML normally via renderFunctionComponent
3. Queues hydration data (module ID, export name, serialized props) on
the Request's clientBoundaryQueue
4. During flushCompletedQueues(), emits <script data-fused-hydration>
tags containing the queued hydration data as JSON
New functions in ReactFizzConfigDOM.js:
- pushStartClientBoundary(target, id): emits <!--C:ID-->
- pushEndClientBoundary(target): emits <!--/C-->
- writeClientBoundaryScript(dest, id, moduleId, name, props):
emits <script> with hydration payload
Props serialization uses JSON.stringify with a replacer that strips
children and functions. TIM-476 will replace this with a focused
serializer handling edge cases (dates, server actions, etc.).
Tests cover: marker wrapping, hydration script emission, unique IDs
for multiple boundaries, nested server-in-client-in-server, props
serialization, and fusedMode=false producing no markers.
…eact#12)
Add ReactFizzHydrationSerializer.js — a ~150-line focused serializer
that handles common prop types at client boundaries without reimplementing
Flight's 800-line renderModelDestructive.
Supported types:
- Primitives (string, number, boolean, null)
- undefined → {$t: 'u'}
- NaN → {$t: 'N'}, ±Infinity → {$t: 'I', v: ±1}
- BigInt → {$t: 'n', v: '...'} (as string)
- Date → {$t: 'D', v: ISO string}
- Plain objects and arrays (recursive)
- Server Action refs → {$t: 'S', id, bound}
- Client references in props → {$t: 'C', id}
- children → {$t: 'T'} tombstone (server-rendered)
- Regular functions → stripped (undefined)
- Symbols → stripped
Throws clear error on: Map, Set, TypedArray, ReadableStream
Wired into renderClientBoundary() in ReactFizzServer.js, replacing
the placeholder JSON.stringify from TIM-475.
…ing (react#13)
Added fused mode path to concurrent benchmark. Results show:
- Fused is 1.0-1.6x faster than full pipeline (modest improvement)
- But only 23-45% of Fizz-only ceiling (expected ~90%)
- Heap pressure is much better: 66MB vs 284MB at c=50
- p95/p99 are comparable or worse due to serializeProps overhead
Root cause: serializeProps runs processObject recursively for each of
226 product props at client boundaries, doing essentially the same
serialization work Flight does. The CPU savings from eliminating Flight
serialize/deserialize are offset by the hydration data serialization.
The server-only path (no client boundaries) IS fast — 2300 req/s.
The bottleneck is specifically the per-boundary props serialization.
Also fixes: added client boundary function exports to legacy, markup,
custom, and noop Fizz config forks (required for production build).
Add fast path to serializeProps that uses a single JSON.stringify call
with a lightweight replacer for the common case (all JSON-native types).
Falls back to manual string building only when special types are
detected (BigInt, undefined, NaN, Infinity, references).
The serializer itself was not the bottleneck — V8's JSON.stringify is
already fast. The real cost for client-heavy pages is the volume of
output: 226 boundaries × ~1.5KB of hydration scripts = 290KB of
additional output per request. This is a payload optimization problem,
not a CPU optimization problem.
Server-only fused rendering (no client boundaries) hits 1,935 req/s
vs 131 req/s for the full Flight pipeline — a 15x improvement. This
is the core proof that single-pass Fizz rendering eliminates the
Flight overhead.
Integration tests proving fused SSR and Flight server coexist:
- fusedMode=false produces identical output to default (no markers)
- fusedMode=true emits boundary markers and hydration scripts
- Alternating fused/standard renders in the same process works
- Concurrent fused and standard renders don't interfere
- Per-request fusedMode gating — no shared state between requests
- Sentinel test: ReactFlightServer.js contains no fused-mode code
The Flight server path is completely unmodified. fusedMode is a
per-request flag on Fizz's Request object only.
…eact#16)
Edge case tests (10 tests):
- Server component children inside client boundary (tombstoned)
- Nested client-in-client with correct marker nesting
- Deeply nested boundaries with unique IDs
- Server component throw caught by Fizz error handling
- Error inside client boundary is handled
- Sync parent with async child via Suspense
- Streaming: shell before async resolves
- Map/Set props fall back gracefully
- Server Action references preserved in hydration data
Sentinel tests (7 tests):
- Client reference protocol (Symbol, shape)
- Server reference protocol (Symbol, shape)
- Fizz has fusedMode code
- Hydration walker skips unknown comments
- Flight server has zero fused-mode code
design/fused-renderer-findings.md:
- Complete findings doc with honest numbers
- renderToString baseline: 485 req/s
- Full pipeline: 108 req/s
- Fused server-only: 514 req/s (4.8x, matches renderToString)
- Fused with client boundaries: 121 req/s (1.1x, hydration data bottleneck)
- Measurement journey: v1→v2→v3→final, documenting each mistake
- Open items: hydration data optimization, client-side approach
- Architecture diagram and usage example
…roughput (react#17)
The props were the entire bottleneck. 226 client boundaries × ~1.2 KB
of serialized props = 227 KB of hydration scripts, capping throughput
at ~121 req/s (same as full pipeline).
Changes:
- renderClientBoundary: skip serializeProps, emit empty props placeholder
- flushCompletedQueues: emit one consolidated <script data-fused-hydration>
instead of 226 individual scripts. Format: {m: [moduleUrls], b: [[id, moduleIdx]]}
- Module ref deduplication in consolidated script
Results at c=25 (226-product PLP):
Full pipeline: 92 req/s, p50=205ms, heap 193MB
Fused renderer: 327 req/s, p50=3.2ms, heap 40MB (3.6x faster)
At c=1:
Full pipeline: 131 req/s
Fused renderer: 576 req/s (4.4x faster, exceeds Fizz-only ceiling)
Props delivery to the client is deferred to the hydration strategy
(TIM-485). The server emits markers + module refs. The client will
get props via DOM extraction, lazy fetch, or a future compact format.
…rs (react#18)
Restore props serialization (skipping props breaks hydration).
Update findings doc with final validated concurrent throughput:
c=1: 127 → 330 req/s (2.6x)
c=25: 98 → 292 req/s (3.0x)
c=50: 89 → 285 req/s (3.2x)
Props serialization adds ~1.3ms per request for 226 boundaries.
This is real, irreducible work — the client needs props to hydrate.
V8 JSON.stringify is already near-optimal. Total per-request cost
is 3.2ms (309 req/s), which is 2.3x faster than the full pipeline.
Server-only fused: 528 req/s (matches renderToString).
…eact#19)
The client reference proxy created by clientExports() is function(){}
with $$typeof set — calling it returns undefined. The fused renderer
was emitting empty boundary markers, benchmarking 'nothing' as fast.
Fix: renderClientBoundary now calls bundlerConfig.resolveClientComponent(
$$id) to get the actual module, equivalent to __webpack_require__ in
the Flight client path. Benchmark uses O(1) Map lookup.
Verified numbers with identical HTML output:
c=1: 128 → 200 req/s (1.6x)
c=25: 99 → 187 req/s (1.9x)
c=50: 93 → 185 req/s (2.0x)
Heap: 297 → 60 MB at c=50
Audit confirms: HTML is byte-identical, components are called,
product names present, module resolution is O(1).
…mat (react#20)
Replace custom resolveClientComponent with standard bundler protocol:
- bundlerConfig is the client manifest (same object as Flight's webpackMap)
- Module resolution via __webpack_require__(metadata.id) — same as Flight client
- Hydration data uses Flight's module ref format: [id, chunks, name, props]
- Emitted as self.__FUSED={b:{...}} — parseable by existing RSC client runtime
Per-request: 7.33ms (full pipeline) → 4.78ms (fused) = 1.5x faster
Concurrent throughput improvement reduced by props serialization output
volume (451 KB fused vs 120 KB full pipeline). The CPU savings from
eliminating Flight are real but offset by generating 3.7x more bytes.
Next step: investigate reducing props output volume to recover the
concurrent throughput advantage.
The full pipeline benchmark was only measuring HTML output, not the
Flight payload that must be inlined as a <script> for client hydration.
This made the comparison unfair — full pipeline at 120 KB vs fused at
451 KB.
Now both paths include hydration data delivery cost:
- Full pipeline: Flight serialize + deserialize + Fizz + inline payload = 7.74ms
- Fused: single pass with props serialization = 4.20ms
- Savings: 3.54ms (46%), 1.84x speedup per-request
The savings come from eliminating Flight serialize (3.24ms) + deserialize
(0.32ms) + payload inlining (0.65ms) = 4.21ms, offset by ~2.2ms of
props serialization in the fused path.

@wyattloveslife-createwyattloveslife-create left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Render

@switz
switz requested review from wyattloveslife-create and removed request for wyattloveslife-createJuly 17, 2026 19:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@switz@wyattloveslife-create