deer-flow/frontend/scripts/measure-route-assets.mjs
DanielWalnut 459dd78707
perf(frontend): bound delivery, bundles, and long-running UI work (#4622)
* docs: design frontend performance remediation

* docs: plan frontend performance remediation

* test(frontend): add route asset performance budgets

* perf(nginx): compress textual responses safely

* perf(frontend): lazy load case study media

* perf(frontend): bound static demo file tracing

* perf(frontend): restore static locale boundaries

* perf(frontend): defer closed workspace panels

* perf(frontend): split editors and deduplicate highlighting

* perf(frontend): index incremental message derivation

* perf(frontend): stabilize paged history cache policy

* perf(frontend): bound streaming markdown renders

* perf(frontend): virtualize message history

* perf(frontend): bound and virtualize chat lists

* perf(frontend): suspend inactive decorative animation

* perf(browser): stream latest frames as binary

* perf(artifacts): stream bounded text previews

* docs: finalize performance runtime boundaries

* style(backend): apply test formatting

* fix(frontend): keep translation functions client-side

* perf(frontend): defer decorative animation bundles

* test(frontend): lock optimized route budgets

* fix: harden frontend performance boundaries

* test(frontend): update i18n provider fixture

* fix(frontend): preserve sidebar pagination position

* style(backend): format artifact range test
2026-08-01 22:19:59 +08:00

250 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { readFile, stat, writeFile } from "node:fs/promises";
import net from "node:net";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const DEMO_THREAD_ID = "7cfa5f8f-a2f8-47ad-acbd-da7137baf990";
export const ROUTES = [
"/",
"/login",
"/workspace/chats",
`/workspace/chats/${DEMO_THREAD_ID}`,
"/en/docs",
"/blog/posts",
];
/**
* @param {boolean} staticWebsiteOnly
* @param {Readonly<Record<string, string | undefined>>} baseEnvironment
*/
export function createBuildEnvironment(
staticWebsiteOnly,
baseEnvironment = process.env,
) {
const environment = { ...baseEnvironment };
if (staticWebsiteOnly) {
environment.NEXT_PUBLIC_STATIC_WEBSITE_ONLY = "true";
} else {
delete environment.NEXT_PUBLIC_STATIC_WEBSITE_ONLY;
}
return environment;
}
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const frontendDir = path.resolve(scriptDir, "..");
export function extractAssetPaths(html) {
const assets = { css: [], js: [] };
const seen = { css: new Set(), js: new Set() };
const attributePattern = /(?:src|href)=["']([^"']+)["']/g;
for (const match of html.matchAll(attributePattern)) {
const value = match[1];
if (!value?.startsWith("/_next/static/")) continue;
const relativePath = value.slice("/_next/static/".length).split("?", 1)[0];
if (!relativePath) continue;
const kind = relativePath.endsWith(".css")
? "css"
: relativePath.endsWith(".js")
? "js"
: null;
if (kind && !seen[kind].has(relativePath)) {
seen[kind].add(relativePath);
assets[kind].push(relativePath);
}
}
return assets;
}
export function evaluateBudgets(measurements, budgets) {
const failures = [];
for (const [route, limits] of Object.entries(budgets)) {
const actual = measurements[route];
if (!actual) {
failures.push(`${route}: route was not measured`);
continue;
}
for (const kind of ["css", "js"]) {
if (actual[kind] <= limits[kind]) continue;
const overage = actual[kind] - limits[kind];
failures.push(
`${route} ${kind}: ${actual[kind]} bytes exceeds ${limits[kind]} bytes by ${overage} ${overage === 1 ? "byte" : "bytes"}`,
);
}
}
return failures;
}
export function formatMeasurementSummary(measurements) {
const lines = ["Route asset summary"];
for (const [route, measurement] of Object.entries(measurements)) {
lines.push(
`${route} [${measurement.buildMode}] JS ${measurement.js} B | CSS ${measurement.css} B | HTML ${measurement.html} B`,
);
}
return lines.join("\n");
}
async function getFreePort() {
return await new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Failed to allocate a local port"));
return;
}
const { port } = address;
server.close((error) => (error ? reject(error) : resolve(port)));
});
});
}
function run(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: frontendDir,
env: process.env,
stdio: "inherit",
...options,
});
child.on("error", reject);
child.on("exit", (code, signal) => {
if (code === 0) resolve();
else reject(new Error(`${command} exited with ${code ?? signal}`));
});
});
}
async function waitForServer(baseUrl, child) {
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`Next.js server exited with ${child.exitCode}`);
}
try {
const response = await fetch(baseUrl, { redirect: "manual" });
if (response.status < 500) return;
} catch {
// The server is still starting.
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error("Timed out waiting for the Next.js production server");
}
async function measureRoute(baseUrl, route) {
const response = await fetch(`${baseUrl}${route}`, { redirect: "manual" });
if (!response.ok) {
throw new Error(`${route} returned HTTP ${response.status}`);
}
const html = await response.text();
const assets = extractAssetPaths(html);
const totals = { css: 0, js: 0 };
for (const kind of ["css", "js"]) {
for (const relativePath of assets[kind]) {
const assetPath = path.join(frontendDir, ".next", "static", relativePath);
totals[kind] += (await stat(assetPath)).size;
}
}
return { ...totals, assets, html: Buffer.byteLength(html) };
}
async function measureBuild(routes, staticWebsiteOnly) {
const env = createBuildEnvironment(staticWebsiteOnly);
await run("pnpm", ["exec", "next", "build"], {
env,
});
const port = await getFreePort();
const server = spawn(
"pnpm",
[
"exec",
"next",
"start",
"--hostname",
"127.0.0.1",
"--port",
String(port),
],
{
cwd: frontendDir,
env,
stdio: ["ignore", "inherit", "inherit"],
},
);
try {
const baseUrl = `http://127.0.0.1:${port}`;
await waitForServer(baseUrl, server);
const measurements = {};
for (const route of routes) {
measurements[route] = {
...(await measureRoute(baseUrl, route)),
buildMode: staticWebsiteOnly ? "static-demo" : "normal",
};
}
return measurements;
} finally {
if (server.exitCode === null) {
await new Promise((resolve) => {
const forceStop = setTimeout(() => server.kill("SIGKILL"), 5_000);
server.once("exit", () => {
clearTimeout(forceStop);
resolve();
});
server.kill("SIGTERM");
});
}
}
}
async function main() {
const check = process.argv.includes("--check");
const measurements = {
...(await measureBuild(["/login"], false)),
...(await measureBuild(
ROUTES.filter((route) => route !== "/login"),
true,
)),
};
await writeFile(
path.join(frontendDir, ".next", "performance-results.json"),
`${JSON.stringify(measurements, null, 2)}\n`,
);
process.stdout.write(`${JSON.stringify(measurements, null, 2)}\n`);
process.stdout.write(`${formatMeasurementSummary(measurements)}\n`);
if (check) {
const budgets = JSON.parse(
await readFile(
path.join(frontendDir, "performance-budgets.json"),
"utf8",
),
);
const failures = evaluateBudgets(measurements, budgets);
if (failures.length > 0) {
throw new Error(`Route asset budgets failed:\n${failures.join("\n")}`);
}
}
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.stack : String(error)}\n`,
);
process.exitCode = 1;
});
}