Skip to content

Commit d138760

Browse files
committed
fix(scripts): 顶层 pass 改总体裁决并修正容差叙述
1 parent 8fc8719 commit d138760

2 files changed

Lines changed: 19 additions & 17 deletions

File tree

‎docs/standards/performance.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
- **口径**:懒加载 chunk(仅动态可达的 `.js`)单独判定 —— ① **族前缀清单等式**(每个懒侧 chunk 必须归属某个已登记族,**未归族即红**;键用**族前缀**如 `vendor-katex-*`,因为 Vite 的 chunk 名带内容 hash、不稳定)② **逐族 gzip 上限** ③ **懒侧总 gzip 上限**(另加 chunk 数上限)。三条都只许降。
3939
- **基线来源**:`scripts/lazyBudget.json` —— 首次写入值 = 2026-09-13 **真构建**实测 **36 chunk / 634.32 kB**(**冻结现状,不是目标值**;写乐观「预算值」会立刻红)。
4040
- 🔴 **口径:本判据只在「新鲜构建」的产物上成立** —— 旧 `dist` 带着上一个时代的残渣(实测:顶层 katex 被删后旧产物仍多 **64,497 B** gzip)⇒ **`--no-build` 的读数不可用于本判据**;要判既有产物就显式 `--dist` 指认(不给 `--dist` 的 `--no-build` 只判首屏,懒侧打 `⏭ 未判`)。
41-
- **硬约束**:上表「页面包大小 (JS) < 200KB (gzip)」与懒侧预算是**两个独立读数** —— 懒侧判定不得影响首屏读数,两者也不得并成一个预算。
41+
- **容差**:逐族与总量判据都用「上限 + **64 B**」—— 它是**测量噪声余量**(本机**同源重建实测 0 B**;参照批 4 的 CSS 顺序差 9 B),**不是**「漂移推导」;常量,**不得**随提交变大(改它 = 改判据)。
42+
- **硬约束**:上表「页面包大小 (JS) < 200KB (gzip)」与懒侧预算是**两个独立读数** —— 懒侧判定不得影响首屏读数,两者也不得并成一个预算。`--json` 的顶层 `pass` = **总体裁决**(两个读数的与),与退出码一致。
43+
- **逐族上限可「同提交重冻」**(批 7 §C32.3):改动懒侧字节的单元可在**同一条提交**里把受影响族的 `gzipBytesMax` 重冻为新实测值(条件:提交信息点名是哪处改动推高 · 报告给前后逐键 diff · **总量不得抬高** · 评审核「是该次改动的必然后果」)。🔴 **懒侧总上限仍是硬棘轮、只许降**。
4244
- **工具**:`node scripts/check-bundle-budget.mjs`(不带 `--no-build` ⇒ 自行构建后再判两个预算,人类可读、**分别**打印两个 pass);加 `--json` 得平级的 `firstScreen` / `lazyBudget` 两栏。退出码 `1` = **任一**预算超标(首屏 / 懒侧)。
4345

4446
### 第二部分:测量与 Profiling

‎scripts/check-bundle-budget.mjs‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
* 🔴 **口径:本判据只在新鲜构建的产物上成立** —— 旧 `dist` 带着上一个时代的残渣(实测:顶层 katex 被删后旧
2121
* 产物仍多 **64,497 B** gzip)⇒ **`--no-build` 的读数不可用于本判据**(要判既有产物就显式 `--dist` 指认)。
2222
* 首屏与懒侧是**两个独立读数**:懒侧判定不得影响首屏字段。
23-
* 5. **容差(批 7 §C25.4)**:逐族与**总量**判据都用「上限 + `TOLERANCE`」,取 `max(1 × 同源构建漂移, 64 B)`。
24-
* 实测漂移两路并列:本单元**两棵同源树各构建一次 = 逐族与总量 0 B**(chunk 名集合逐字相同)· R-4 在 HEAD 上读 **−18 B**
25-
* ⇒ `max(1 × 18, 64)` = **64 B**。🔴 常量、不得随提交变大(改它 = 改判据);人类输出与 `--json` 都打印它。
23+
* 5. **容差(批 7 §C25.4 / §C32.2)**:逐族与**总量**判据都用「上限 + `TOLERANCE` = **64 B**」;它是**测量噪声余量**
24+
* 而非「漂移推导」:本机**同源重建实测 0 B**(本单元两棵树 · R-6 两对树逐族全等、105/105 文件 sha256 相同),
25+
* 参照批 4 曾测的 CSS 顺序差 **9 B** ⇒ 取 **64 B 保守余量**。🔴 常量、不得随提交变大;人类输出与 `--json` 都打印它。
26+
* 6. **逐族上限可「同提交重冻」(§C32.3,推翻 §C25.4(b) 的否决)**:改懒侧字节的单元**可在同一条提交里**把受影响族的
27+
* `gzipBytesMax` 重冻为新实测值,条件 = ① 提交信息逐字点明哪处改动推高 ② 报告给前后逐键 diff(旧→新→实测)
28+
* ③ 🔴 **懒侧总上限不得抬高** ④ 评审核「是该次改动的必然后果,不是顺手抬预算」。**总量仍是硬棘轮、只许降**。
2629
*
2730
* 仪器陷阱(本批实测,勿改回):
2831
* - 静态边在产物里长 `from"./x-abc.js"`:**引号必须紧跟** `from` / `import`,故 `import(` 天然不匹配;
@@ -35,8 +38,7 @@
3538
* 副作用:默认执行 `cd app && npm run build`(写 `app/dist/`);`--no-build` 只读既有产物。
3639
* `--self-test` 在 `os.tmpdir()` 下建夹具并在结束时删除,**不碰真实 `app/dist`**。
3740
* 用法:node scripts/check-bundle-budget.mjs [--no-build] [--budget <kB>] [--json] [--self-test] [--dist <目录>] [--no-lazy]
38-
* `--dist` 供自检夹具 / 备用产物目录使用(默认 `app/dist`),与构建互斥、须配 `--no-build`。给 `--dist`
39-
* 即视作**你把该产物作为受判对象**(懒侧判据据此启用);不给则 `--no-build` **只判首屏**(懒侧见下条)。
41+
* `--dist` 供自检夹具 / 备用产物目录使用(默认 `app/dist`),与构建互斥、须配 `--no-build`;给 `--dist` 即视作你把该产物作为受判对象(懒侧判据据此启用),不给则 `--no-build` **只判首屏**(懒侧见下条)。
4042
* `--json` 与默认构建同用会把 vite 输出混进 stdout;机器消费请配 `--no-build`。
4143
* `--no-lazy` **只**给 `--dist` 的夹具用(人工造的夹具不可能命中真实基线),且**不得**指向默认产物目录。
4244
* 退出码:0 = **首屏与懒侧两个预算都达标** · 1 = **任一预算超标(首屏 / 懒侧)** ·
@@ -67,9 +69,9 @@ const BUDGET_SOURCE = "docs/standards/performance.md:28";
6769
const LAZY_BUDGET = join(dirname(SELF), "lazyBudget.json");
6870
// 懒侧判据的**合格口径 = 新鲜构建**(§C21.3):① 不带 `--no-build` ⇒ 本次真构建;② 显式 `--dist` ⇒ 操作者把该产物作为受判对象交进来;两者都不满足(裸 `--no-build`)⇒ 只判首屏,懒侧**不下判定**。
6971
const JUDGE_LAZY = !NO_LAZY && (!NO_BUILD || has("--dist"));
70-
// 容差(§C25.4):`max(1 × 同源构建漂移, 64 B)`。漂移实测 = 本单元 0 B(两棵同源树)· R-4 −18 B ⇒ 取包住两者的 18 B ⇒ 64 B 下限生效。
71-
// 🔴 常量、**不得**随任何提交变大(改它 = 改判据,须回报控制方);`k = 1`,两个读数见文件头第 5 条。
72-
const TOLERANCE = Math.max(1 * 18, 64);
72+
// 容差(§C25.4 / §C32.2):**测量噪声余量 64 B** —— 同源重建实测 0 B(本单元 + R-6),参照批 4 的 CSS 顺序差 9 B。
73+
// 🔴 常量、**不得**随任何提交变大(改它 = 改判据,须回报控制方);依据与用法见文件头第 5 / 6 条。
74+
const TOLERANCE = 64;
7375

7476
/** 全部失败路径的统一出口:**必须** exit 2(1 只留给「超预算」),并给出可执行的下一条命令。 */
7577
function fail(msg) {
@@ -85,7 +87,6 @@ function loadLazyBudget(p) {
8587
if (!ok) fail(`懒侧基线 ${rel(p)} 结构不完整(需要 families[{prefix,gzipBytesMax}] + lazyTotalGzipBytesMax + lazyChunkCountMax)。`);
8688
return b;
8789
}
88-
8990
/** 族前缀 = 去掉**8 位定长**内容 hash 的 chunk 名 + `-`。hash 字母表含 `-`(实测 `CaptureFloatPanel-CJ-lF7pj.js`)。 */
9091
const HASH_SUFFIX = /-[A-Za-z0-9_-]{8}\.js$/;
9192
const familyPrefixOf = (name) => (HASH_SUFFIX.test(name) ? name.replace(HASH_SUFFIX, "") + "-" : null);
@@ -100,8 +101,7 @@ function judgeLazy(r, base) {
100101
for (const c of r.lazy) {
101102
const f = fams.get(familyPrefixOf(c.name));
102103
if (!f) { unlisted.push(c.name); continue; }
103-
f.chunks.push(c.name);
104-
f.gzipBytes += c.gzipBytes;
104+
f.chunks.push(c.name); f.gzipBytes += c.gzipBytes;
105105
}
106106
const families = [...fams.values()]; const totalBytes = r.lazy.reduce((a, c) => a + c.gzipBytes, 0);
107107
const reasons = [];
@@ -132,8 +132,8 @@ function printHuman(r, pass, lazy) {
132132
console.log(` ⇒ 首屏预算 ${pass ? "✅ 达标" : "❌ 超标"}:${pass ? "余量" : "超出"} ${kB(Math.abs(BUDGET_KB * 1000 - r.eagerBytes))} kB${pass ? "" : `(${(r.eagerBytes / 1000 / BUDGET_KB).toFixed(2)}× 预算)`}`);
133133
console.log(`懒加载 chunk(仅动态可达,独立于首屏预算):${r.lazy.length} 个 · gzip ${fmtB(r.lazy.reduce((a, c) => a + c.gzipBytes, 0))} B = ${kB(r.lazy.reduce((a, c) => a + c.gzipBytes, 0))} kB`);
134134
if (lazy) {
135-
console.log(` ⇒ 懒侧预算 ${lazy.pass ? "✅ 达标" : "❌ 超标"}:gzip ${kB(lazy.totalBytes)} kB / 上限 ${kB(lazy.totalMax)} kB + 容差 ${lazy.tolerance} B · chunk ${lazy.count} 个 / 上限 ${lazy.countMax} 个 · 未归族 ${lazy.unlisted.length} 个 · 超上限族 ${lazy.overFamilies.length} 个(基线 ${rel(LAZY_BUDGET)},实测冻结值只许降;容差 = max(1 × 同源构建漂移, 64 B))`);
136-
} else console.log(` ⇒ 懒侧预算 ⏭ 未判(${NO_LAZY ? "--no-lazy:自检夹具" : "--no-build 的读数不可用于本判据(旧产物会多算 ~64 kB)—— 去掉 --no-build 让本脚本代跑构建,或显式 --dist 指认受判产物"})`);
135+
console.log(` ⇒ 懒侧预算 ${lazy.pass ? "✅ 达标" : "❌ 超标"}:gzip ${kB(lazy.totalBytes)} kB / 上限 ${kB(lazy.totalMax)} kB + 容差 ${lazy.tolerance} B · chunk ${lazy.count} 个 / 上限 ${lazy.countMax} 个 · 未归族 ${lazy.unlisted.length} 个 · 超上限族 ${lazy.overFamilies.length} 个(基线 ${rel(LAZY_BUDGET)},实测冻结值只许降;容差 = 测量噪声余量,同源重建实测 0 B)`);
136+
} else console.log(` ⇒ 懒侧预算 ⏭ 未判(容差 ${TOLERANCE} B;${NO_LAZY ? "--no-lazy:自检夹具" : "--no-build 的读数不可用于本判据 —— 去掉 --no-build 让本脚本代跑构建,或显式 --dist 指认受判产物"})`);
137137
console.log(`不计入预算但须报告(CSS / 字体):index.html ${fmtB(r.htmlBytes)} B = ${kB(r.htmlBytes)} kB · gzip ${kB(r.htmlGzip)} kB`);
138138
const byExt = new Map();
139139
for (const a of r.excluded) {
@@ -175,12 +175,12 @@ const jsonOf = (r, pass, lazy) => ({
175175
}
176176
: { judged: false, pass: null, tolerance: TOLERANCE, skipped: NO_LAZY ? "--no-lazy(自检夹具)" : "--no-build 的读数不可用于懒侧判据(口径要求新鲜构建;亦可在显式 --dist 下判定)" },
177177
excludedFromBudget: { indexHtml: { bytes: r.htmlBytes, gzipBytes: r.htmlGzip }, assets: r.excluded },
178-
pass,
178+
pass: pass && (!lazy || lazy.pass),
179179
});
180180

181181
/**
182-
* 报表 + 退出码:0 = **两个预算都达标** · 1 = **任一超标**(首屏 / 懒侧,各自点名)。顶层 `pass` 是**首屏** pass
183-
* (既有消费方的兼容字段);懒侧见 `lazyBudget.pass`(`judged:false` 时它是 `null`,不是「达标」)。
182+
* 报表 + 退出码:0 = **两个预算都达标** · 1 = **任一超标**(首屏 / 懒侧,各自点名)。🔴 顶层 `pass` = **总体裁决**
183+
* (`firstScreen.pass && lazyBudget.pass`,与退出码**必须一致**);两个独立字段仍分别保留在各自对象里。
184184
*/
185185
function finish(r) {
186186
const pass = r.eagerBytes / 1000 < BUDGET_KB;

0 commit comments

Comments
 (0)