'use strict'; require('@babel/register')({ presets: [['@babel/preset-react', {runtime: 'automatic'}]], plugins: ['@babel/plugin-transform-modules-commonjs'], only: [/\/src\//], }); const path = require('path'); const fs = require('fs'); const webpack = require('webpack'); const inspector = require('node:inspector'); const {clientManifest, ssrManifest} = require('./webpack-mock'); const PROFILE_MODE = process.argv.includes('--profile'); const CONCURRENT_MODE = process.argv.includes('--concurrent'); const INJECT = !process.argv.includes('--no-injection'); const JSON_OUT = (function () { const arg = process.argv.find(function (a) { return a.startsWith('--json-out='); }); return arg ? arg.slice('--json-out='.length) : null; })(); const jsonResults = []; function writeJsonOut(mode) { if (!JSON_OUT) return; fs.writeFileSync(JSON_OUT, JSON.stringify({mode, results: jsonResults})); } // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- function build() { const config = require('./webpack.config'); return new Promise(function (resolve, reject) { webpack(config, function (err, stats) { if (err) { reject(err); return; } if (stats.hasErrors()) { reject(new Error(stats.toString({errors: true}))); return; } console.log( stats.toString({colors: true, modules: false, entrypoints: false}) ); resolve(); }); }); } // --------------------------------------------------------------------------- // Render helpers // --------------------------------------------------------------------------- const { renderFizzNode: renderFizzNodeStream, renderFizzEdge: renderFizzEdgeStream, renderFlightFizzNode: renderFlightFizzNodeStream, renderFlightFizzEdge: renderFlightFizzEdgeStream, nodeStreamToString, webStreamToString, } = require('./render-helpers'); const {printGrid} = require('./print-helpers'); function renderFizzNode(AppComponent, itemCount) { return nodeStreamToString(renderFizzNodeStream(AppComponent, itemCount)); } function renderFizzEdge(AppComponent, itemCount) { return renderFizzEdgeStream(AppComponent, itemCount).then(webStreamToString); } function renderFlightFizzNode(renderRSCNode, AppComponent, itemCount) { return nodeStreamToString( renderFlightFizzNodeStream( renderRSCNode, AppComponent, itemCount, clientManifest, ssrManifest, {inject: INJECT} ) ); } function renderFlightFizzEdge(renderRSCEdge, AppComponent, itemCount) { return renderFlightFizzEdgeStream( renderRSCEdge, AppComponent, itemCount, clientManifest, ssrManifest, {inject: INJECT} ).then(webStreamToString); } // --------------------------------------------------------------------------- // Benchmarking // --------------------------------------------------------------------------- const canGC = typeof globalThis.gc === 'function'; // Yield to the event loop's check phase between renders. React schedules a // setImmediate per request, but a fully in-process render completes entirely // in microtasks/nextTicks, so a tight benchmark loop never lets those // immediates run. They then pile up in Node's immediate queue, each one // retaining its (otherwise finished) request graph, which inflates any memory // measurement. A real server yields to the event loop on every request, so // draining between iterations is both correct and more representative. function tick() { return new Promise(resolve => setImmediate(resolve)); } async function settledHeapUsed() { await tick(); if (canGC) globalThis.gc(); return process.memoryUsage().heapUsed; } async function runBenchmark(name, fn, iterations, warmup) { if (canGC) globalThis.gc(); const heapBefore = await settledHeapUsed(); // Warmup for (let i = 0; i < warmup; i++) { await fn(); await tick(); } // Collect GC pauses during timed iterations. let gcCount = 0; let gcTotalMs = 0; const gcObs = new PerformanceObserver(list => { for (const entry of list.getEntries()) { gcCount++; gcTotalMs += entry.duration; } }); gcObs.observe({entryTypes: ['gc']}); // Timed iterations. The tick between iterations is excluded from the // timed window. const times = []; for (let i = 0; i < iterations; i++) { const start = performance.now(); await fn(); times.push(performance.now() - start); await tick(); } gcObs.disconnect(); const heapAfter = await settledHeapUsed(); // Trim top/bottom 5% to remove outliers const sorted = [...times].sort((a, b) => a - b); const trimCount = Math.floor(sorted.length * 0.05); const trimmed = sorted.slice(trimCount, sorted.length - trimCount); const mean = trimmed.reduce((s, t) => s + t, 0) / trimmed.length; const median = sorted[Math.floor(sorted.length / 2)]; const stddev = Math.sqrt( trimmed.reduce((s, t) => s + (t - mean) ** 2, 0) / trimmed.length ); const p95 = sorted[Math.floor(sorted.length * 0.95)]; const min = sorted[0]; const max = sorted[sorted.length - 1]; return { name, mean, median, stddev, p95, min, max, iterations, gcCount, gcTotalMs, heapBefore, heapAfter, }; } function printResult(result) { jsonResults.push(result); console.log(' %s:', result.name); console.log(' Mean: %s ms', result.mean.toFixed(2)); console.log(' Median: %s ms', result.median.toFixed(2)); console.log(' Stddev: %s ms', result.stddev.toFixed(2)); console.log(' P95: %s ms', result.p95.toFixed(2)); console.log(' Min: %s ms', result.min.toFixed(2)); console.log(' Max: %s ms', result.max.toFixed(2)); console.log( ' GC: %d pauses, %s ms total (%s ms/iter)', result.gcCount, result.gcTotalMs.toFixed(1), (result.gcTotalMs / result.iterations).toFixed(2) ); printHeap(result); } function printHeap(result) { if (!canGC) return; const mb = b => (b / 1048576).toFixed(1); const delta = result.heapAfter - result.heapBefore; console.log( ' Heap: %s MB retained after run (%s%s MB vs before)', mb(result.heapAfter), delta >= 0 ? '+' : '', mb(delta) ); } async function runConcurrent(name, fn, total, concurrency, warmup) { if (canGC) globalThis.gc(); const heapBefore = await settledHeapUsed(); for (let i = 0; i < warmup; i++) { await fn(); await tick(); } let gcCount = 0; let gcTotalMs = 0; const gcObs = new PerformanceObserver(list => { for (const entry of list.getEntries()) { gcCount++; gcTotalMs += entry.duration; } }); gcObs.observe({entryTypes: ['gc']}); const latencies = new Array(total); let completed = 0; let launched = 0; const start = performance.now(); await new Promise(resolve => { function launch() { while (launched < total && launched - completed < concurrency) { const idx = launched++; const t0 = performance.now(); fn() .then(() => { latencies[idx] = performance.now() - t0; // Drain immediates before freeing the slot (see tick()). return tick(); }) .then(() => { completed++; if (completed === total) { resolve(); } else { launch(); } }); } } launch(); }); const elapsed = performance.now() - start; gcObs.disconnect(); const heapAfter = await settledHeapUsed(); const sorted = [...latencies].sort((a, b) => a - b); const mean = sorted.reduce((s, t) => s + t, 0) / sorted.length; const p95 = sorted[Math.floor(sorted.length * 0.95)]; return { name, reqPerSec: (total / elapsed) * 1000, mean, p95, total, concurrency, gcCount, gcTotalMs, heapBefore, heapAfter, }; } function printConcurrentResult(result) { jsonResults.push(result); console.log(' %s:', result.name); console.log(' Req/s: %s', result.reqPerSec.toFixed(1)); console.log(' Mean: %s ms', result.mean.toFixed(2)); console.log(' P95: %s ms', result.p95.toFixed(2)); console.log( ' GC: %d pauses, %s ms total (%s ms/req)', result.gcCount, result.gcTotalMs.toFixed(1), (result.gcTotalMs / result.total).toFixed(2) ); printHeap(result); } // --------------------------------------------------------------------------- // CPU Profiling // --------------------------------------------------------------------------- function startProfiler() { const session = new inspector.Session(); session.connect(); return new Promise(function (resolve, reject) { session.post('Profiler.enable', function (err) { if (err) { reject(err); return; } session.post('Profiler.start', function (err2) { if (err2) { reject(err2); return; } resolve(session); }); }); }); } function stopProfiler(session, outputPath) { return new Promise(function (resolve, reject) { session.post('Profiler.stop', function (err, {profile}) { if (err) { reject(err); return; } fs.mkdirSync(path.dirname(outputPath), {recursive: true}); fs.writeFileSync(outputPath, JSON.stringify(profile)); session.post('Profiler.disable'); session.disconnect(); resolve(profile); }); }); } function printTopFunctions(profile, topN) { // Aggregate self-time per function from the profile nodes. const selfTimes = new Map(); for (const node of profile.nodes) { const name = node.callFrame.functionName || '(anonymous)'; const loc = node.callFrame.url ? node.callFrame.url.replace(/.*\//, '') + ':' + node.callFrame.lineNumber : '(native)'; const key = name + ' @ ' + loc; const hitCount = node.hitCount || 0; selfTimes.set(key, (selfTimes.get(key) || 0) + hitCount); } const sorted = [...selfTimes.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, topN); const totalSamples = profile.nodes.reduce((s, n) => s + (n.hitCount || 0), 0); console.log(' Top %d functions by self-time:', topN); for (const [key, hits] of sorted) { const pct = ((hits / totalSamples) * 100).toFixed(1); console.log(' %s%% - %s', pct, key); } } async function profileRun(name, fn, warmup, iterations, outputPath) { // Warmup (unprofiled) for (let i = 0; i < warmup; i++) { await fn(); await tick(); } // Collect GC pauses during the profiled run. let gcCount = 0; let gcTotalMs = 0; const gcObs = new PerformanceObserver(list => { for (const entry of list.getEntries()) { gcCount++; gcTotalMs += entry.duration; } }); gcObs.observe({entryTypes: ['gc']}); // Profiled run const session = await startProfiler(); for (let i = 0; i < iterations; i++) { await fn(); await tick(); } const profile = await stopProfiler(session, outputPath); gcObs.disconnect(); console.log(' %s → %s', name, outputPath); printTopFunctions(profile, 10); console.log( ' GC: %d pauses, %s ms total (%s ms/iter)', gcCount, gcTotalMs.toFixed(1), (gcTotalMs / iterations).toFixed(2) ); } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main() { console.log('Building RSC bundle...\n'); await build(); const { renderRSCNode, renderRSCEdge, App: RSCApp, AppAsync: RSCAppAsync, } = require('./build/rsc-bundle.js'); const App = require('./src/App.js').default; const AppAsync = require('./src/AppAsync.js').default; const ITEM_COUNT = 200; const WARMUP = 50; const ITERATIONS = 1000; const PROFILE_WARMUP = 50; const PROFILE_ITERATIONS = 500; // --- Verify renders --- console.log('\n--- Verifying renders ---\n'); const fizzNodeHtml = await renderFizzNode(App, ITEM_COUNT); console.log('Fizz (Node, sync): %d bytes', fizzNodeHtml.length); const flightFizzNodeHtml = await renderFlightFizzNode( renderRSCNode, RSCApp, ITEM_COUNT ); console.log( 'Flight + Fizz (Node, sync): %d bytes', flightFizzNodeHtml.length ); const fizzNodeAsyncHtml = await renderFizzNode(AppAsync, ITEM_COUNT); console.log('Fizz (Node, async): %d bytes', fizzNodeAsyncHtml.length); const flightFizzNodeAsyncHtml = await renderFlightFizzNode( renderRSCNode, RSCAppAsync, ITEM_COUNT ); console.log( 'Flight + Fizz (Node, async):%d bytes', flightFizzNodeAsyncHtml.length ); const fizzEdgeHtml = await renderFizzEdge(App, ITEM_COUNT); console.log('Fizz (Edge, sync): %d bytes', fizzEdgeHtml.length); const fizzEdgeAsyncHtml = await renderFizzEdge(AppAsync, ITEM_COUNT); console.log('Fizz (Edge, async): %d bytes', fizzEdgeAsyncHtml.length); const flightFizzEdgeHtml = await renderFlightFizzEdge( renderRSCEdge, RSCApp, ITEM_COUNT ); console.log( 'Flight + Fizz (Edge, sync): %d bytes', flightFizzEdgeHtml.length ); const flightFizzEdgeAsyncHtml = await renderFlightFizzEdge( renderRSCEdge, RSCAppAsync, ITEM_COUNT ); console.log( 'Flight + Fizz (Edge, async):%d bytes', flightFizzEdgeAsyncHtml.length ); // --- CPU Profiling --- if (PROFILE_MODE) { console.log( '\n--- CPU Profiling (%d warmup, %d iterations) ---\n', PROFILE_WARMUP, PROFILE_ITERATIONS ); const profileDir = path.resolve(__dirname, 'build/profiles'); await profileRun( 'Fizz (Node, sync)', () => renderFizzNode(App, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'fizz-node-sync.cpuprofile') ); await profileRun( 'Flight + Fizz (Node, sync)', () => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'flight-fizz-node-sync.cpuprofile') ); await profileRun( 'Fizz (Node, async)', () => renderFizzNode(AppAsync, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'fizz-node-async.cpuprofile') ); await profileRun( 'Flight + Fizz (Node, async)', () => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'flight-fizz-node-async.cpuprofile') ); await profileRun( 'Fizz (Edge, sync)', () => renderFizzEdge(App, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'fizz-edge-sync.cpuprofile') ); await profileRun( 'Flight + Fizz (Edge, sync)', () => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'flight-fizz-edge-sync.cpuprofile') ); await profileRun( 'Fizz (Edge, async)', () => renderFizzEdge(AppAsync, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'fizz-edge-async.cpuprofile') ); await profileRun( 'Flight + Fizz (Edge, async)', () => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT), PROFILE_WARMUP, PROFILE_ITERATIONS, path.join(profileDir, 'flight-fizz-edge-async.cpuprofile') ); console.log( '\nProfiles saved to build/profiles/. Open in Chrome DevTools or speedscope.app.' ); return; } // --- Concurrent Benchmark --- if (CONCURRENT_MODE) { const CONCURRENCY = 50; const TOTAL = 1000; const CONC_WARMUP = 20; console.log( '\n--- Concurrent Benchmark (%d warmup, %d concurrency, %d requests, %d items) ---\n', CONC_WARMUP, CONCURRENCY, TOTAL, ITEM_COUNT ); const fizzNodeSync = await runConcurrent( 'Fizz (Node, sync)', () => renderFizzNode(App, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(fizzNodeSync); const flightFizzNodeSync = await runConcurrent( 'Flight + Fizz (Node, sync)', () => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(flightFizzNodeSync); const fizzNodeAsync = await runConcurrent( 'Fizz (Node, async)', () => renderFizzNode(AppAsync, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(fizzNodeAsync); const flightFizzNodeAsync = await runConcurrent( 'Flight + Fizz (Node, async)', () => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(flightFizzNodeAsync); const fizzEdgeSync = await runConcurrent( 'Fizz (Edge, sync)', () => renderFizzEdge(App, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(fizzEdgeSync); const flightFizzEdgeSync = await runConcurrent( 'Flight + Fizz (Edge, sync)', () => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(flightFizzEdgeSync); const fizzEdgeAsync = await runConcurrent( 'Fizz (Edge, async)', () => renderFizzEdge(AppAsync, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(fizzEdgeAsync); const flightFizzEdgeAsync = await runConcurrent( 'Flight + Fizz (Edge, async)', () => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT), TOTAL, CONCURRENCY, CONC_WARMUP ); printConcurrentResult(flightFizzEdgeAsync); const rps = r => r.reqPerSec; console.log('\n--- Flight overhead ---\n'); printGrid( ['Fizz', 'Flight+Fizz'], [ ['Node sync', fizzNodeSync, flightFizzNodeSync], ['Node async', fizzNodeAsync, flightFizzNodeAsync], ['Edge sync', fizzEdgeSync, flightFizzEdgeSync], ['Edge async', fizzEdgeAsync, flightFizzEdgeAsync], ], rps, 'req/s', 'higher is better' ); console.log('\n--- Edge vs Node ---\n'); printGrid( ['Node', 'Edge'], [ ['Fizz sync', fizzNodeSync, fizzEdgeSync], ['Fizz async', fizzNodeAsync, fizzEdgeAsync], ['Flight+Fizz sync', flightFizzNodeSync, flightFizzEdgeSync], ['Flight+Fizz async', flightFizzNodeAsync, flightFizzEdgeAsync], ], rps, 'req/s', 'higher is better' ); writeJsonOut('concurrent'); return; } // --- Benchmark --- console.log( '\n--- Benchmark (%d warmup, %d iterations, %d items) ---\n', WARMUP, ITERATIONS, ITEM_COUNT ); const fizzNodeSync = await runBenchmark( 'Fizz (Node, sync)', () => renderFizzNode(App, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(fizzNodeSync); const flightFizzNodeSync = await runBenchmark( 'Flight + Fizz (Node, sync)', () => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(flightFizzNodeSync); const fizzNodeAsync = await runBenchmark( 'Fizz (Node, async)', () => renderFizzNode(AppAsync, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(fizzNodeAsync); const flightFizzNodeAsync = await runBenchmark( 'Flight + Fizz (Node, async)', () => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(flightFizzNodeAsync); const fizzEdgeSync = await runBenchmark( 'Fizz (Edge, sync)', () => renderFizzEdge(App, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(fizzEdgeSync); const flightFizzEdgeSync = await runBenchmark( 'Flight + Fizz (Edge, sync)', () => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(flightFizzEdgeSync); const fizzEdgeAsync = await runBenchmark( 'Fizz (Edge, async)', () => renderFizzEdge(AppAsync, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(fizzEdgeAsync); const flightFizzEdgeAsync = await runBenchmark( 'Flight + Fizz (Edge, async)', () => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT), ITERATIONS, WARMUP ); printResult(flightFizzEdgeAsync); const median = r => r.median; console.log('\n--- Flight overhead ---\n'); printGrid( ['Fizz', 'Flight+Fizz'], [ ['Node sync', fizzNodeSync, flightFizzNodeSync], ['Node async', fizzNodeAsync, flightFizzNodeAsync], ['Edge sync', fizzEdgeSync, flightFizzEdgeSync], ['Edge async', fizzEdgeAsync, flightFizzEdgeAsync], ], median, 'ms', 'median, lower is better' ); console.log('\n--- Edge vs Node ---\n'); printGrid( ['Node', 'Edge'], [ ['Fizz sync', fizzNodeSync, fizzEdgeSync], ['Fizz async', fizzNodeAsync, fizzEdgeAsync], ['Flight+Fizz sync', flightFizzNodeSync, flightFizzEdgeSync], ['Flight+Fizz async', flightFizzNodeAsync, flightFizzEdgeAsync], ], median, 'ms', 'median, lower is better' ); writeJsonOut(INJECT ? 'inject' : 'bare'); } main().catch(function (err) { console.error(err); process.exit(1); });