From ed347098b4aedc4545edb4f14fd9d9946f5321b1 Mon Sep 17 00:00:00 2001 From: mes123456 Date: Sun, 6 Sep 2026 10:46:53 +0800 Subject: [PATCH] perf(web): skip unchanged incremental web builds --- docs/WEB_BUILD_EXECUTION_OPTIMIZATION.md | 3 ++ tools/web/incremental-web-build.mjs | 23 +++++++++ tools/web/run-incremental-typecheck.mjs | 18 +++++++ tools/web/web-build-fingerprint.mjs | 61 ++++++++++++++++++++++++ web/package.json | 4 +- 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 tools/web/incremental-web-build.mjs create mode 100644 tools/web/run-incremental-typecheck.mjs create mode 100644 tools/web/web-build-fingerprint.mjs diff --git a/docs/WEB_BUILD_EXECUTION_OPTIMIZATION.md b/docs/WEB_BUILD_EXECUTION_OPTIMIZATION.md index 33f58c55b..2d16a3ca8 100644 --- a/docs/WEB_BUILD_EXECUTION_OPTIMIZATION.md +++ b/docs/WEB_BUILD_EXECUTION_OPTIMIZATION.md @@ -23,6 +23,9 @@ - Emscripten 环境、无人值守启动器和原生构建入口统一使用自适应并行度,避免在高配机器上固定 `-j8` 或 `-j2`。 - `web/tsconfig.json` 开启增量图和 `.cache/tsconfig.tsbuildinfo`,使独立 `typecheck` 与后续 `build` 共享 TypeScript 解析结果。 +- `npm run typecheck` 和 `npm run build` 使用源码/配置指纹。指纹未变化且 + 构件存在时输出 `web-typecheck-reused`/`web-build-reused`,不重复启动 + TypeScript 或 Vite;任一输入变化都会自动回到完整增量构建路径。 - 调度器状态显示为 WASM 构件增加 `bytes + mtime_ns` 指纹缓存;未变化时不再重复读取并计算 SHA-256。 - 状态页从既有收据计算并显示当前阶段中位数最高的观测瓶颈,便于下一轮只调整真正耗时的阶段。 diff --git a/tools/web/incremental-web-build.mjs b/tools/web/incremental-web-build.mjs new file mode 100644 index 000000000..61cb804e6 --- /dev/null +++ b/tools/web/incremental-web-build.mjs @@ -0,0 +1,23 @@ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import { fingerprint, readState, root, writeState } from "./web-build-fingerprint.mjs"; + +const currentFingerprint = fingerprint(); +const state = readState("web-build-success.json"); +const index = `${root}/web/dist/index.html`; +if (state?.fingerprint === currentFingerprint && fs.existsSync(index)) { + process.stdout.write(`web-build-reused fingerprint=${currentFingerprint}\n`); + process.exit(0); +} + +const typecheck = spawnSync(process.execPath, ["tools/web/run-incremental-typecheck.mjs"], { + cwd: root, + stdio: "inherit", +}); +if (typecheck.status !== 0) process.exit(typecheck.status ?? 1); +const vite = spawnSync(process.execPath, ["./node_modules/vite/bin/vite.js", "build", "--config", "app/vite.config.ts"], { + cwd: `${root}/web`, + stdio: "inherit", +}); +if (vite.status !== 0) process.exit(vite.status ?? 1); +writeState("web-build-success.json", { schemaVersion: 1, fingerprint: currentFingerprint }); diff --git a/tools/web/run-incremental-typecheck.mjs b/tools/web/run-incremental-typecheck.mjs new file mode 100644 index 000000000..ffa3dc30e --- /dev/null +++ b/tools/web/run-incremental-typecheck.mjs @@ -0,0 +1,18 @@ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import { fingerprint, readState, root, statePath, writeState } from "./web-build-fingerprint.mjs"; + +const currentFingerprint = fingerprint(); +const state = readState("typecheck-success.json"); +const buildInfo = statePath("tsconfig.tsbuildinfo"); +if (state?.fingerprint === currentFingerprint && fs.existsSync(buildInfo)) { + process.stdout.write(`web-typecheck-reused fingerprint=${currentFingerprint}\n`); + process.exit(0); +} + +const result = spawnSync(process.execPath, ["./node_modules/typescript/bin/tsc", "-p", "tsconfig.json"], { + cwd: root + "/web", + stdio: "inherit", +}); +if (result.status !== 0) process.exit(result.status ?? 1); +writeState("typecheck-success.json", { schemaVersion: 1, fingerprint: currentFingerprint }); diff --git a/tools/web/web-build-fingerprint.mjs b/tools/web/web-build-fingerprint.mjs new file mode 100644 index 000000000..5e0db6aa0 --- /dev/null +++ b/tools/web/web-build-fingerprint.mjs @@ -0,0 +1,61 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const webRoot = path.join(root, "web"); + +function walk(directory, output) { + if (!fs.existsSync(directory)) return; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".cache" || entry.name === ".vite") continue; + const file = path.join(directory, entry.name); + if (entry.isDirectory()) walk(file, output); + else if (entry.isFile()) output.push(file); + } +} + +export function inputFiles() { + const files = [ + path.join(webRoot, "package.json"), + path.join(webRoot, "package-lock.json"), + path.join(webRoot, "tsconfig.json"), + path.join(webRoot, "app/vite.config.ts"), + ]; + walk(path.join(webRoot, "app/src"), files); + walk(path.join(webRoot, "protocol"), files); + walk(path.join(webRoot, "app/public"), files); + return [...new Set(files)].filter((file) => fs.existsSync(file)).sort(); +} + +export function fingerprint() { + const hash = crypto.createHash("sha256"); + for (const file of inputFiles()) { + const stat = fs.statSync(file); + hash.update(path.relative(root, file)); + hash.update("\0"); + hash.update(String(stat.size)); + hash.update("\0"); + hash.update(String(stat.mtimeNs)); + hash.update("\n"); + } + return hash.digest("hex"); +} + +export function statePath(name) { + return path.join(webRoot, ".cache", name); +} + +export function readState(name) { + try { return JSON.parse(fs.readFileSync(statePath(name), "utf8")); } + catch { return null; } +} + +export function writeState(name, value) { + const file = statePath(name); + fs.mkdirSync(path.dirname(file), { recursive: true }); + const temporary = `${file}.tmp-${process.pid}`; + fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); + fs.renameSync(temporary, file); +} diff --git a/web/package.json b/web/package.json index c91fe4af0..46212b3a2 100644 --- a/web/package.json +++ b/web/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "vite --config app/vite.config.ts", - "build": "tsc -p tsconfig.json && vite build --config app/vite.config.ts", - "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "node ../tools/web/incremental-web-build.mjs", + "typecheck": "node ../tools/web/run-incremental-typecheck.mjs", "lint": "eslint app", "check:local-deps": "bash ../tools/web/check-local-deps.sh", "test": "node --test tests/**/*.test.mjs",