diff --git a/benchmarks/perf/bundle-size.mjs b/benchmarks/perf/bundle-size.mjs index 6ef1d4536..94e29f979 100644 --- a/benchmarks/perf/bundle-size.mjs +++ b/benchmarks/perf/bundle-size.mjs @@ -1,8 +1,9 @@ #!/usr/bin/env node import { lstat, readdir, readFile, realpath } from "node:fs/promises"; -import { isAbsolute, join, relative, resolve } from "node:path"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { gzipSync } from "node:zlib"; +import { parseAst } from "vite"; import { reportPerformanceSample } from "./report-sample.mjs"; const repositoryRoot = process.env.VINEXT_PERF_TARGET_ROOT ?? process.cwd(); @@ -109,6 +110,46 @@ async function gzipClientEntryClosure(clientOutputPath) { return { gzipBytes, fileCount: visited.size }; } +async function gzipStaticEntryClosure(entryPath) { + const outputDirectory = dirname(entryPath); + const resolvedOutputDirectory = await realpath(outputDirectory); + let gzipBytes = 0; + const visited = new Set(); + + async function visit(filePath) { + const output = await lstat(filePath); + if (!output.isFile() || output.isSymbolicLink()) { + throw new Error(`RSC entry output must be a regular file: ${filePath}`); + } + const resolvedFilePath = await realpath(filePath); + const outputRelativePath = relative(resolvedOutputDirectory, resolvedFilePath); + if (outputRelativePath.startsWith("..") || isAbsolute(outputRelativePath)) { + throw new Error(`RSC entry import escapes the output directory: ${filePath}`); + } + if (visited.has(resolvedFilePath)) return; + visited.add(resolvedFilePath); + + const source = await readFile(resolvedFilePath, "utf8"); + gzipBytes += gzipSync(source).length; + const ast = parseAst(source); + for (const statement of ast.body) { + if ( + statement.type !== "ImportDeclaration" && + statement.type !== "ExportAllDeclaration" && + statement.type !== "ExportNamedDeclaration" + ) { + continue; + } + const specifier = statement.source?.value; + if (typeof specifier !== "string" || !specifier.startsWith(".")) continue; + await visit(resolve(dirname(filePath), specifier)); + } + } + + await visit(entryPath); + return { gzipBytes, fileCount: visited.size }; +} + async function main() { const output = await lstat(outputPath).catch(() => null); if (output?.isSymbolicLink()) { @@ -133,7 +174,7 @@ async function main() { const { gzipBytes, fileCount } = target === "rsc-entry" - ? { gzipBytes: gzipSync(await readFile(outputPath)).length, fileCount: 1 } + ? await gzipStaticEntryClosure(outputPath) : target === "client-entry" ? await gzipClientEntryClosure(outputPath) : await gzipBundleSize(outputPath); diff --git a/benchmarks/perf/scenarios.json b/benchmarks/perf/scenarios.json index f14ffa544..045c93f31 100644 --- a/benchmarks/perf/scenarios.json +++ b/benchmarks/perf/scenarios.json @@ -101,8 +101,8 @@ { "id": "rsc-entry-gzip", "suite": "Build", - "label": "RSC entry size (gzip)", - "description": "Gzip size of the vinext production RSC entry emitted at dist/server/index.js.", + "label": "RSC entry closure size (gzip)", + "description": "Total gzip size of the vinext production RSC entry and its eager static JavaScript imports.", "unit": "bytes", "lowerIsBetter": true, "implementations": [ diff --git a/tests/performance-benchmarks.test.ts b/tests/performance-benchmarks.test.ts index 33c3ab155..65340413f 100644 --- a/tests/performance-benchmarks.test.ts +++ b/tests/performance-benchmarks.test.ts @@ -265,18 +265,23 @@ describe("paired performance benchmarks", () => { ).toThrow("Bundle output escapes the benchmark checkout"); }); - it("measures the RSC entry and complete server bundle separately", () => { + it("measures the eager RSC entry closure and complete server bundle separately", () => { const directory = mkdtempSync(join(tmpdir(), "vinext-perf-server-bundle-")); const serverDirectory = join(directory, "benchmarks/vinext/dist/server"); const samplesPath = join(directory, "samples.jsonl"); - const rscEntry = "export default function handler() { return 'rsc'; }"; + const rscEntry = + "import './_next/static/shared.js'; export { value } from './_next/static/reexport.js'; import('./_next/static/lazy.js'); export default function handler() { return 'rsc'; }"; const ssrEntry = "export function render() { return 'ssr'; }"; - const sharedChunk = "export const shared = 'chunk';"; + const sharedChunk = "import '../../index.js'; export const shared = 'chunk';"; + const reexportedChunk = "export const value = 'reexported';"; + const lazyChunk = "export const lazy = 'lazy';"; mkdirSync(join(serverDirectory, "ssr"), { recursive: true }); mkdirSync(join(serverDirectory, "_next/static"), { recursive: true }); writeFileSync(join(serverDirectory, "index.js"), rscEntry); writeFileSync(join(serverDirectory, "ssr/index.js"), ssrEntry); writeFileSync(join(serverDirectory, "_next/static/shared.js"), sharedChunk); + writeFileSync(join(serverDirectory, "_next/static/reexport.js"), reexportedChunk); + writeFileSync(join(serverDirectory, "_next/static/lazy.js"), lazyChunk); writeFileSync(join(serverDirectory, "vinext-server.json"), "{}"); const baseEnvironment = { @@ -307,8 +312,12 @@ describe("paired performance benchmarks", () => { .split("\n") .map((line) => JSON.parse(line)); expect(samples.map((sample) => sample.value)).toEqual([ - gzipSync(rscEntry).length, - gzipSync(rscEntry).length + gzipSync(ssrEntry).length + gzipSync(sharedChunk).length, + gzipSync(rscEntry).length + gzipSync(sharedChunk).length + gzipSync(reexportedChunk).length, + gzipSync(rscEntry).length + + gzipSync(ssrEntry).length + + gzipSync(sharedChunk).length + + gzipSync(reexportedChunk).length + + gzipSync(lazyChunk).length, ]); });