Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scripts/bench-builds.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './benchmark/dev-server.mjs';
import { generateSyntheticFixture } from './benchmark/fixture.mts';
import { profiles } from './benchmark/profiles.mjs';
import { collectBuildOutputStats } from './benchmark/bundle-size.mjs';
import {
parsePluginReports,
parseTimeStats,
Expand Down Expand Up @@ -550,6 +551,12 @@ const runBenchmarkIteration = (benchmarkContext, index) =>
const pluginReports = parsePluginReports(
`${commandResult.stdout}\n${commandResult.stderr}`
);
const bundleSize =
args.mode === 'build' && measured && commandResult.status === 0
? yield* tryPromise(() =>
collectBuildOutputStats(path.join(fixtureRoot, 'build'))
)
: null;

if (commandResult.status !== 0 && args.failFast) {
yield* Effect.fail(
Expand All @@ -574,6 +581,7 @@ const runBenchmarkIteration = (benchmarkContext, index) =>
userMs: timeStats.userMs ?? null,
sysMs: timeStats.sysMs ?? null,
maxRssKb: timeStats.maxRssKb ?? null,
bundleSize,
pluginReports,
rspackProfiles,
rspackTraceOutput:
Expand Down
108 changes: 108 additions & 0 deletions scripts/benchmark/bundle-size.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { gzip as gzipCallback } from 'node:zlib';

const gzip = promisify(gzipCallback);

export const bundleSizeMetricKeys = [
'fileCount',
'totalBytes',
'totalGzipBytes',
'clientBytes',
'clientGzipBytes',
'clientJsBytes',
'clientJsGzipBytes',
'clientCssBytes',
'clientCssGzipBytes',
'serverBytes',
'serverGzipBytes',
'sourceMapBytes',
'sourceMapGzipBytes',
];

export const createEmptyBuildOutputStats = () =>
Object.fromEntries(bundleSizeMetricKeys.map(key => [key, 0]));

export const formatBytes = value => {
if (value == null) {
return '-';
}
if (value >= 1024 * 1024) {
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
return `${(value / 1024).toFixed(1)} kB`;
};

export const getBundleSizeMedian = (benchmark, key) =>
benchmark?.summary?.bundleSize?.[key]?.median ?? null;

export const summarizeBundleSizeRuns = (runs, summarizeMetric) =>
Object.fromEntries(
bundleSizeMetricKeys.map(key => [
key,
summarizeMetric(runs.map(run => run.bundleSize?.[key])),
])
);

const addFileStats = (stats, relativePath, bytes, gzipBytes) => {
stats.fileCount += 1;
stats.totalBytes += bytes;
stats.totalGzipBytes += gzipBytes;

const normalizedPath = relativePath.split(path.sep).join('/');
const isSourceMap = normalizedPath.endsWith('.map');

if (isSourceMap) {
stats.sourceMapBytes += bytes;
stats.sourceMapGzipBytes += gzipBytes;
}

if (normalizedPath.startsWith('client/')) {
stats.clientBytes += bytes;
stats.clientGzipBytes += gzipBytes;
if (normalizedPath.endsWith('.js')) {
stats.clientJsBytes += bytes;
stats.clientJsGzipBytes += gzipBytes;
} else if (normalizedPath.endsWith('.css')) {
stats.clientCssBytes += bytes;
stats.clientCssGzipBytes += gzipBytes;
}
} else if (normalizedPath.startsWith('server/')) {
stats.serverBytes += bytes;
stats.serverGzipBytes += gzipBytes;
}
};

export const collectBuildOutputStats = async buildRoot => {
const stats = createEmptyBuildOutputStats();

const visit = async directory => {
const entries = await readdir(directory, { withFileTypes: true });

await Promise.all(
entries.map(async entry => {
const filePath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await visit(filePath);
return;
}
if (!entry.isFile()) {
return;
}

const buffer = await readFile(filePath);
const gzipped = await gzip(buffer);
addFileStats(
stats,
path.relative(buildRoot, filePath),
buffer.byteLength,
gzipped.byteLength
);
})
);
};

await visit(buildRoot);
return stats;
};
10 changes: 6 additions & 4 deletions scripts/benchmark/ci-report-markdown.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { formatBytes } from './bundle-size.mjs';

const formatSeconds = value =>
typeof value === 'number' ? `${(value / 1000).toFixed(2)}s` : '-';
const formatPercent = value =>
Expand Down Expand Up @@ -26,8 +28,8 @@ const formatSignal = value =>
})[value] ?? '⚠️ insufficient data';

const productionBenchmarkTableHeader = [
'| Benchmark | Runs | Base total | Head total | Delta | Head mean | Head p95 | Speedup | Head RSS p95 |',
'|---|---:|---:|---:|---:|---:|---:|---:|---:|',
'| Benchmark | Runs | Base total | Head total | Delta | Head mean | Head p95 | Speedup | Head RSS p95 | Head client JS gzip | Client JS gzip delta | Head total gzip |',
'|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|',
];

const devBenchmarkTableHeader = [
Expand Down Expand Up @@ -92,13 +94,13 @@ const appendSignalSummary = (lines, report) => {
};

const renderBuildBenchmarkRow = benchmark =>
`| \`${benchmark.id}\` | ${formatCount(benchmarkRunCount(benchmark))} | ${formatSeconds(benchmark.baseWallMs)} | ${formatSeconds(benchmark.headWallMs)} | ${formatPercent(benchmark.wallDeltaPercent)} | ${formatSeconds(benchmark.headWallMeanMs)} | ${formatSeconds(benchmark.headWallP95Ms)} | ${formatSpeedup(benchmark.wallSpeedup)} | ${formatRss(benchmark.headRssKb)} |`;
`| \`${benchmark.id}\` | ${formatCount(benchmarkRunCount(benchmark))} | ${formatSeconds(benchmark.baseWallMs)} | ${formatSeconds(benchmark.headWallMs)} | ${formatPercent(benchmark.wallDeltaPercent)} | ${formatSeconds(benchmark.headWallMeanMs)} | ${formatSeconds(benchmark.headWallP95Ms)} | ${formatSpeedup(benchmark.wallSpeedup)} | ${formatRss(benchmark.headRssKb)} | ${formatBytes(benchmark.headClientJsGzipBytes)} | ${formatPercent(benchmark.clientJsGzipDeltaPercent)} | ${formatBytes(benchmark.headTotalGzipBytes)} |`;

const renderDevBenchmarkRow = benchmark =>
`| \`${benchmark.id}\` | ${formatCount(benchmarkRunCount(benchmark))} | ${formatSeconds(benchmark.baseWallMs)} | ${formatSeconds(benchmark.headWallMs)} | ${formatPercent(benchmark.wallDeltaPercent)} | ${formatSeconds(benchmark.baseReadyMs)} | ${formatSeconds(benchmark.headReadyMs)} | ${formatSeconds(benchmark.baseRouteTotalMs)} | ${formatSeconds(benchmark.headRouteTotalMs)} | ${formatSeconds(benchmark.baseUpdateMs)} | ${formatSeconds(benchmark.headUpdateMs)} | ${formatPercent(benchmark.updateDeltaPercent)} | ${formatSeconds(benchmark.headWallMeanMs)} | ${formatSeconds(benchmark.headWallP95Ms)} | ${formatSpeedup(benchmark.wallSpeedup)} | ${formatRss(benchmark.headRssKb)} |`;

const renderSyntheticBuildBenchmarkRow = benchmark =>
`| complex app | ${formatCount(benchmark.runs)} | ${formatDurationSeconds(benchmark.baseMedianSeconds)} | ${formatDurationSeconds(benchmark.headMedianSeconds)} | ${formatPercent(benchmark.deltaPercent)} | ${formatDurationSeconds(benchmark.headMeanSeconds)} | ${formatDurationSeconds(benchmark.headP95Seconds)} | ${formatSpeedup(benchmark.speedup)} | - |`;
`| complex app | ${formatCount(benchmark.runs)} | ${formatDurationSeconds(benchmark.baseMedianSeconds)} | ${formatDurationSeconds(benchmark.headMedianSeconds)} | ${formatPercent(benchmark.deltaPercent)} | ${formatDurationSeconds(benchmark.headMeanSeconds)} | ${formatDurationSeconds(benchmark.headP95Seconds)} | ${formatSpeedup(benchmark.speedup)} | - | - | - | - |`;

const renderSyntheticDevBenchmarkRow = benchmark =>
`| complex app | ${formatCount(benchmark.runs)} | ${formatDurationSeconds(benchmark.baseMedianSeconds)} | ${formatDurationSeconds(benchmark.headMedianSeconds)} | ${formatPercent(benchmark.deltaPercent)} | ${formatSeconds(benchmark.baseReadyMs)} | ${formatSeconds(benchmark.headReadyMs)} | ${formatSeconds(benchmark.baseRouteTotalMs)} | ${formatSeconds(benchmark.headRouteTotalMs)} | ${formatSeconds(benchmark.baseUpdateMs)} | ${formatSeconds(benchmark.headUpdateMs)} | ${formatPercent(benchmark.updateDeltaPercent)} | ${formatDurationSeconds(benchmark.headMeanSeconds)} | ${formatDurationSeconds(benchmark.headP95Seconds)} | ${formatSpeedup(benchmark.speedup)} | - |`;
Expand Down
25 changes: 25 additions & 0 deletions scripts/benchmark/ci-report-model.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { getBundleSizeMedian } from './bundle-size.mjs';
import { classifyBenchmarkSignal } from './statistics.mjs';

export const readJson = async file => JSON.parse(await readFile(file, 'utf8'));
Expand Down Expand Up @@ -145,6 +146,22 @@ const compareBenchmarks = (baseResult, headResult) => {
const headRouteTotalMs = medianRouteTotal(headBenchmark);
const baseUpdateMs = medianUpdate(baseBenchmark);
const headUpdateMs = medianUpdate(headBenchmark);
const baseClientJsGzipBytes = getBundleSizeMedian(
baseBenchmark,
'clientJsGzipBytes'
);
const headClientJsGzipBytes = getBundleSizeMedian(
headBenchmark,
'clientJsGzipBytes'
);
const baseTotalGzipBytes = getBundleSizeMedian(
baseBenchmark,
'totalGzipBytes'
);
const headTotalGzipBytes = getBundleSizeMedian(
headBenchmark,
'totalGzipBytes'
);
const stability = classifyBenchmarkSignal(
(baseBenchmark?.runs ?? []).map(run => run.wallMs),
(headBenchmark?.runs ?? []).map(run => run.wallMs)
Expand All @@ -171,6 +188,14 @@ const compareBenchmarks = (baseResult, headResult) => {
headCpuMs: cpuMedian(headBenchmark),
baseRssKb: p95Rss(baseBenchmark),
headRssKb: p95Rss(headBenchmark),
baseClientJsGzipBytes,
headClientJsGzipBytes,
clientJsGzipDeltaPercent: percentDelta(
baseClientJsGzipBytes,
headClientJsGzipBytes
),
baseTotalGzipBytes,
headTotalGzipBytes,
baseRunCount: baseBenchmark?.runs?.length ?? null,
headRunCount: headBenchmark?.runs?.length ?? null,
headWallMeanMs: headBenchmark?.summary?.wallMs?.mean ?? null,
Expand Down
8 changes: 6 additions & 2 deletions scripts/benchmark/results.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { formatBytes, summarizeBundleSizeRuns } from './bundle-size.mjs';

export const parseTimeStats = stderr => {
const user = stderr.match(/User time \(seconds\):\s*([\d.]+)/);
Expand Down Expand Up @@ -64,6 +65,7 @@ export const summarizeRuns = runs => ({
userMs: summarizeMetric(runs.map(run => run.userMs)),
sysMs: summarizeMetric(runs.map(run => run.sysMs)),
maxRssKb: summarizeMetric(runs.map(run => run.maxRssKb)),
bundleSize: summarizeBundleSizeRuns(runs, summarizeMetric),
});

const summarizeDevRequests = (runs, key) => {
Expand Down Expand Up @@ -237,8 +239,8 @@ export const renderMarkdown = result => {
? [`- Rspack trace output: ${result.rspackTraceOutput}`]
: []),
'',
'| Benchmark | Routes | Variant | Median ready | Median route load | Median update/HMR | Median wall | Mean wall | p95 wall | Max RSS | Plugin reports (--log-performance) |',
'|---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|',
'| Benchmark | Routes | Variant | Median ready | Median route load | Median update/HMR | Median wall | Mean wall | p95 wall | Max RSS | Client JS gzip | Total gzip | Plugin reports (--log-performance) |',
'|---|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|',
];

for (const benchmark of result.benchmarks) {
Expand All @@ -254,6 +256,8 @@ export const renderMarkdown = result => {
formatMs(benchmark.summary.wallMs.mean),
formatMs(benchmark.summary.wallMs.p95),
formatRss(benchmark.summary.maxRssKb.p95),
formatBytes(benchmark.summary.bundleSize.clientJsGzipBytes.median),
formatBytes(benchmark.summary.bundleSize.totalGzipBytes.median),
benchmark.runs.reduce((sum, run) => sum + run.pluginReports.length, 0),
]
.join(' | ')
Expand Down
13 changes: 13 additions & 0 deletions scripts/compare-benchmarks.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { parseArgs } from 'node:util';
import { readJson } from './benchmark/ci-report-model.mjs';
import { formatBytes } from './benchmark/bundle-size.mjs';

const parseInput = () => {
const { values } = parseArgs({
Expand Down Expand Up @@ -113,6 +114,18 @@ const main = async () => {
after: metric(afterBenchmark, 'summary.maxRssKb.p95'),
format: formatKb,
},
{
label: 'Client JS gzip median',
before: metric(beforeBenchmark, 'summary.bundleSize.clientJsGzipBytes.median'),
after: metric(afterBenchmark, 'summary.bundleSize.clientJsGzipBytes.median'),
format: formatBytes,
},
{
label: 'Total output gzip median',
before: metric(beforeBenchmark, 'summary.bundleSize.totalGzipBytes.median'),
after: metric(afterBenchmark, 'summary.bundleSize.totalGzipBytes.median'),
format: formatBytes,
},
];

for (const operation of operations) {
Expand Down
18 changes: 6 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ import { createReactRouterDevRuntimeController } from './dev-runtime-controller.
import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
import { registerReactRouterTypegen } from './typegen.js';
import { importConfigWithWatchPaths } from './config-imports.js';
import {
createWebOptimizationConfig,
createWebOutputConfig,
} from './web-rspack-config.js';
import {
createReactRouterRouteTopology,
createReactRouterRouteWatchFiles,
Expand Down Expand Up @@ -875,18 +879,8 @@ export const pluginReactRouter = (
}
: {}),
externalsType: 'module',
output: {
chunkFormat: 'module',
chunkLoading: 'import',
workerChunkLoading: 'import',
wasmLoading: 'fetch',
library: { type: 'module' },
module: true,
},
optimization: {
avoidEntryIife: true,
runtimeChunk: 'single',
},
output: createWebOutputConfig(isBuild),
optimization: createWebOptimizationConfig(isBuild),
},
},
},
Expand Down
30 changes: 30 additions & 0 deletions src/web-rspack-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Rspack } from '@rsbuild/core';

export const createWebOutputConfig = (
isBuild: boolean
): NonNullable<Rspack.Configuration['output']> => ({
chunkFormat: 'module',
chunkLoading: 'import',
...(isBuild
? {
chunkFilename: 'static/js/async/[id]-[contenthash:16].js',
}
: {}),
workerChunkLoading: 'import',
wasmLoading: 'fetch',
library: { type: 'module' },
module: true,
});

export const createWebOptimizationConfig = (
isBuild: boolean
): NonNullable<Rspack.Configuration['optimization']> => ({
avoidEntryIife: true,
...(isBuild
? {
mangleExports: 'size',
usedExports: 'global',
}
: {}),
runtimeChunk: 'single',
});
Loading
Loading