Skip to content
Merged
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
45 changes: 43 additions & 2 deletions benchmarks/perf/bundle-size.mjs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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()) {
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/perf/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
19 changes: 14 additions & 5 deletions tests/performance-benchmarks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
]);
});

Expand Down
Loading