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(` 天然不匹配;
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";
6769const LAZY_BUDGET = join ( dirname ( SELF ) , "lazyBudget.json" ) ;
6870// 懒侧判据的**合格口径 = 新鲜构建**(§C21.3):① 不带 `--no-build` ⇒ 本次真构建;② 显式 `--dist` ⇒ 操作者把该产物作为受判对象交进来;两者都不满足(裸 `--no-build`)⇒ 只判首屏,懒侧**不下判定**。
6971const 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 只留给「超预算」),并给出可执行的下一条命令。 */
7577function 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`)。 */
9091const HASH_SUFFIX = / - [ A - Z a - z 0 - 9 _ - ] { 8 } \. j s $ / ;
9192const 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 */
185185function finish ( r ) {
186186 const pass = r . eagerBytes / 1000 < BUDGET_KB ;
0 commit comments