'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');
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();
});
});
}
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);
}
const canGC = typeof globalThis.gc === 'function';
async function runBenchmark(name, fn, iterations, warmup) {
if (canGC) globalThis.gc();
for (let i = 0; i < warmup; i++) {
await fn();
}
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 times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
await fn();
times.push(performance.now() - start);
}
gcObs.disconnect();
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,
};
}
function printResult(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)
);
}
async function runConcurrent(name, fn, total, concurrency, warmup) {
if (canGC) globalThis.gc();
for (let i = 0; i < warmup; i++) {
await fn();
}
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;
completed++;
if (completed === total) {
resolve();
} else {
launch();
}
});
}
}
launch();
});
const elapsed = performance.now() - start;
gcObs.disconnect();
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,
};
}
function printConcurrentResult(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)
);
}
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) {
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) {
for (let i = 0; i < warmup; i++) {
await fn();
}
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 session = await startProfiler();
for (let i = 0; i < iterations; i++) {
await fn();
}
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)
);
}
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;
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
);
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;
}
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'
);
return;
}
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'
);
}
main().catch(function (err) {
console.error(err);
process.exit(1);
});