| name | pitfalls |
| description | 本项目(Lit 3 + WebGL + WASM·Moonbit 数值内核 + vite/bun 单页游戏)实踩并验证过的疑难杂症避坑手册。当遇到以下信号时使用:布局右溢/间距消失、居中元素宽度只有一半、滚动条异常、刷新后状态丢失、后退/前进不生效、iOS 或移动端卡顿/无声/手势异常、模拟时间判定失效、长测试超时、内核数值位漂移。每条含症状、根因、修法、可提前识别的信号。修问题先查本手册,再查网络。 |
疑难杂症避坑手册
本项目实踩并验证过的坑。每条按「症状 → 根因 → 修法 → 信号」组织。
新增条目:在对应分类追加 ### Xn 小节,并在「快速检索」补一行 症状 → Xn。
快速检索(症状 → 条目)
- 布局右溢、右侧间距消失 → A2、A4
- 居中元素(提示条/弹层)宽度只有容器一半 → B4
- 滚动容器内阴影被截断/缺一截 → B5
- 栏与下方卡片间距消失、视觉贴死 → B6
- iOS standalone 底部白条/Safari 布局被压扁(black-translucent 后出现) → B7
- 移动浏览器居中内容视觉下沉(上方空、下方贴浏览器栏) → B8
- 顶部滚不回去、底部间距不可见 → A3
- 刷新后页面/状态丢失 → C6
- 后退/前进"没反应"、源删不掉 → C5
- 后退后历史条目异常 → C3、C4、C7
- 点链接/跳转后 URL 状态(如 ?dev=1)丢失 → C9
- 返回按钮跳到别的网站/空白页(直达链接场景)→ C10(history.length 不可靠)
- 编辑框撤销/重做失效、滚动回顶(文本还在)→ C11(appendChild 重挂子树)
- iOS 卡顿/掉帧 → D1、D2、D5、D7
- 开局数秒后必现一次百毫秒级卡顿(每局一次、长任务/LoAF 定位单帧 rAF 77ms+) → D6
- Canvas 2D 是历史实现(#7 已迁移),渲染为 WebGL1 批量(WASM·SIMD 内核) → D7
- WebGL 上下文恢复后白屏/资源泄漏 → D9
- 整页白屏且 console 是模块初始化抛错(关卡解析等,非 WebGL) → D11
- shadow DOM 内"按到哪个内部控件"判定失效(拖不动/一按就拖) → A10
- Lit 交互"点了没反应/慢半拍"(状态是普通字段) → A11
- 上下文恢复后地面/天空缺失(动态层正常) → D10
- 地形闪烁/过一会儿整体消失,重置无效刷新恢复(粒子饱和后触发) → D12
- 拖尾透明度呈阶梯/带状、拐弯处有缺口,或每次刷新粒子场不一样 → D13、D14
- 半透明图元端头/接头/头部比主体深(圆帽叠线段、盘叠拖尾、环接头) → D15
- 片元塑形软体(云/光晕)边缘出现水平/垂直直边 → D16
- 重置后旗面/装饰面永久消失、杆还在,只有刷新页面恢复 → D17
- 风向/速度过零时旗面突然消失再弹向另一侧 → D18
- 多实例批只渲染一个/实例间闪现横跳(淡出后异地满显,云瞬移) → D19
- 云/装饰物被吹出可视区后长时间不出现 → G13
- 装饰物在地图边缘堆积/长期停滞,或贴着另一朵生成 → G15、G16
- 高倍速切回低倍速延迟数秒生效(仍快转/仍低帧率) → G17
- 高倍速运行中点暂停/增删源延迟数秒、恢复无延迟、画面不卡 → G21
- 跨线程传了用不上的大场(堆锯齿、真机无感):先算传输/消费量级比,重构前先真机验证 → G22
- 关卡解鲁棒性低/贴地源扰动即失效 → G18
- 淡出/裁剪逻辑让物体整体消失 → D8
- 切后台回前台无声 → F2
- 长按弹系统菜单/双击缩放 → E1
- 右键放错源 → E2
- iPhone 点输入框页面被放大 → E4
- 按钮焦点吞快捷键/Space 双触发 → E5
- 弹窗 Esc 拦截无效/穿透退关 → E6
- 触摸长按放的源被秒删 → E7
- 整数秒时间判定失效 → G1
- vitest 长模拟超时 → G3
- 搜索类算法跑不完 → G4
- 关卡调参/解法搜索:粗筛胜点在精验翻车 → G6
- 参考解"看起来能通"但玩家摆偏 1 格就废 → G7
- 贴地滑行是否计数、贴地飞机能否被风带飞(#25 起与旧认知不同) → G8
- 逐元素断言的测试慢得离谱(断言比被测逻辑还贵) → G9
- 长循环模拟测试慢:资源在循环内重复创建 → G10
- 改了姿态/角度逻辑后物体停稳位置漂移、沿坡蠕爬 → G11
- 网格加偏移后空中被误标固体/流场错位 → G12
- 布局测量与预期不符 → A1、H1
- wasm 卡顿/倍速失效但纯 JS 正常(开着 DevTools 或 IDE 内置浏览器时) → H4
- 挂进宿主元素的覆盖层元素"消失"(getBoundingClientRect 全 0/视口外)→ A9
- CSS/样式改了却不生效(Lit 模板里写了
// 注释) → A12
- 图标/SVG 空白不渲染、DevTools 编辑后恢复(嵌套 html 模板做 svg 子内容) → A13
- 玻璃面文字发虚、插画背景上不可读(半透明面缺 backdrop blur) → A14
- 想用 wasm/代码生成/Worker 加速、移动端"应该更慢"的想当然 → I1、I5
- run-level --verify/--solve 输出"通关 0.0s · 路程 NaN" → I8(bun 运行时 WASM·SIMD 误执行,先验过 vitest)
- 只有 iOS Safari 卡、其他平台都好 → I6(Metal 后端渲染路径)
- headless Chrome 截图/验证画面空白或只有背景色 → I7
- AS/wasm 移植后数值"差不多但不对"(整数字面量相除截断)→ I9
- 改内核数值后通关记录不可复现/逐位对照失败 → I10(golden hash 基线;Moonbit 移植的位级一致方法论)
- vite dev 自动化访问 127.0.0.1 失败(000)→ I2
- bun 跑脚本报 stdio/进程残留/端口占用 → I3
- 站点分发的 .md 文档中文乱码(浏览器按 Latin-1 解码)→ I11
- MCP 自动化布尔断言恒真/深链检查形同虚设 → I12('false' 字符串 truthy;关卡 URL 用 slug 非序号)
- SAB/COI 历史(共享内存注入/COI 头/兼容回退,已整体移除)与教训 → I13 终章;gtag/COEP 权衡同条
- gtag/js 加载 200 却零条 collect(GA 上报静默全断)→ I14(snippet 别做 JS 延迟注入,老实放 head 内联)
- 新增 @types 包后
-p tsconfig.app.json 过但 tsc -b 报找不到类型 → I15(node config 覆盖 types,两处都得加)
- worker 打包丢失(运行期指向未编译 .ts)→ I16(new Worker( 与 new URL( 必须内联字面,抽变量断 Vite 检测)
A. Lit + Shadow DOM
A1 布局测量三件套不一致时先查 transform/zoom
getBoundingClientRect().width(含 transform)、offsetWidth(布局宽)、getComputedStyle().width(CSS 宽)三者应一致;不一致 → 有 transform/zoom/未回流。逐级祖先查 getComputedStyle(el).transform !== 'none'。
A2 全局 * { box-sizing: border-box } 不穿透 shadow DOM
症状:窄屏右溢;width:100% + padding 的盒子比预期宽;max-width 反而把卡片钉在超出视口的宽度。
根因:全局样式表不进影子根,组件内实际是 content-box——width:100% 是内容宽,padding 再加到盒子外,总宽超过 max-width 时被按 border-box 语义压缩,越界。
修法:每个 Lit 组件 static styles 开头自声明 *, *::before, *::after { box-sizing: border-box }。
信号:自定义元素内任何尺寸与预期不符,先查这个。
A3 place-items: center 溢出双向裁切
症状:内容超高时顶部滚不回去、底部内边距看不见。
根因:grid/flex 居中在溢出时两端同时被裁。
修法:容器保留 display: flex(column),子项用 margin: auto——适配时居中,溢出时 margin 塌缩为 0、从顶部可滚动、底部间距可见。
信号:任何"居中 + overflow:auto"的组合。
A4 grid 中百分比宽度循环解析
症状:width: min(35rem, 100%) 或 max-width: 100% 在窄屏解析出比视口还大的值,右溢。
根因:grid 自动轨道按项目 max-content 定尺寸,100% 解析到被撑大的轨道(循环)。
修法:flex 列 + width: 100%; max-width: 35rem; margin: auto;或纯块级 + margin: 0 auto。
信号:单测/隔离复现(内容很小)测不出来——必须用真实内容量级的页面测(H1 探测法)。
A5 装饰器 + useDefineForClassFields: false
@query() 只生成 getter,字段必须 ! 断言且不能带初始化器,否则运行时报 "has only a getter"。
A6 不要在 updated()/firstUpdated() 内设置响应式属性
触发 change-in-update 告警;派生状态用 willUpdate。
A7 事件名必须静态
@hudchange= 可以,@${var}= 不行。
A8 canvas 的 parentElement 恒为 null
canvas 是 shadow root 直接子节点,不能隐式推断宿主,尺寸适配的宿主必须显式传入。
A9 无 <slot> 的 shadow 组件:append 到宿主的 light DOM 子元素不可见
症状:往宿主元素 host.appendChild(el) 挂覆盖层(计时条/弹层),元素在 DOM 树里、querySelector 找得到,但 getBoundingClientRect 返回 0 或视口外坐标(不渲染)。
根因:自定义元素有 shadow root 且 render 模板没有 <slot> 时,light DOM 子元素不参与渲染(shadow 模式默认不显示 light 内容)。
修法:要么给组件加 <slot>,要么把覆盖层挂到 document.body(fixed 定位,与视口对齐;游戏内 app 的 shadow 外挂 perfEl 即此先例)。验证时注意查询路径:body 下直接 document.querySelector,shadow 内要逐层进 shadowRoot。
B. 布局 / 响应式 / 单位
B1 根字号随视口缩放实现全站 rem 适配
html { font-size: calc(12.5px + min(0.7vw, 0.38vh)) }——vh 项即高度侧约束(矮窗自动收紧),不加 clamp;窄屏手机 @media (max-width: 420px) 收紧为 calc(10px + min(0.56vw, 0.3vh))(盖住 360–412 常见安卓与 iPhone 标准/mini)。组件内一律 rem,不再逐屏写断点。
信号:任何新尺寸都用 rem,不用 px(px 仅保留特殊情形:发丝线、动画位移、胶囊 999px、媒体查询断点、env(safe-area)、阴影)。
B2 纵向溢出兜底必须可滚动且不裁切
横屏手机等极矮视口放不下时,正确行为是"可滚动 + 底部间距可见"(见 A3),而不是裁切。能接受极小视口有滚动条,主流屏幕(≥560px 高)应无。
B3 headless Chrome 测量注意
--window-size 是最外层窗口,内层视口更小且有 500px 最小宽。测量用 window.innerWidth/innerHeight,断言布局用注入脚本读 computed style + bounding rect(详见 H)。
C. URL 状态 / 撤销重做
症状:position: absolute; left: 50%; transform: translateX(-50%) 居中的提示条/弹层,max-width: 92% 设了却永远到不了,实际宽度只有容器一半。
根因:无显式宽度的绝对定位元素按 shrink-to-fit 定宽,其可用空间 = 包含块 − left 偏移 = 50%,max-width 只是上限、不是目标宽度。
修法:加 width: max-content(宽度贴内容成胶囊,max-width 恢复封顶换行职责);或直接给显式宽度。
信号:任何 absolute + left:50% + 无 width 的组合,且内容比预期窄。
B5 滚动容器内 box-shadow 被"隐约截断"
症状:滚动容器(overflow: auto/scroll/hidden)内的按钮/卡片阴影在贴容器边缘的方向缺一截,渐变硬切(hud 顶栏按钮最典型)。
根因:overflow ≠ visible 的轴必裁 box-shadow(裁切边界 = 容器 padding box),blur 向四周扩散、不止 offset 方向(向上 = blur−offset、向下 = blur+offset)。overflow-x: clip + 另一轴非 clip 时 clip 被计算为 hidden,overflow-clip-margin 失效——滚动容器内阴影没有 CSS 放行魔法,只能靠留白。裁切边界在视口边缘(容器 box = 视口,如整页滚动容器)时硬边在屏幕外、不可见,属正常现象不必处理。
修法:容器四边 padding ≥ 阴影外扩(offset+blur),四边都要(blur 双向扩散);hud 同时注意 dev-panel 的 top 硬依赖 hud 总高(改 padding 须同步)。
信号:滚动容器内带阴影元素贴边;阴影"刚冒头就被掐断"。
B6 给 margin/padding 加安全区时,多值 shorthand 被误缩成两值,间距悄悄丢失
症状:header bar 与下方卡片贴死(2026-08-08 加 env(safe-area-inset-left/right) 后实测):margin: 0 calc(-1 * (var(--page-pad-x) + env(safe-area-inset-left, 0px))) 原是 margin: 0 calc(...) var(--sp-4) 三值——改写时按两值收尾,bottom 的 var(--sp-4) 被吞(两值 = 上下 0 / 左右 calc),栏与内容的间距归零;且左右两侧误用同一 inset(横屏刘海在单侧,左/右必须各自 inset)。
修法:带安全区的多值 padding/margin 一律写全四值(top/right/bottom/left),右侧用 inset-right、左侧用 inset-left;改完对含 .bar 的屏(storage/dev)探测 card.top − bar.bottom ≥ 设计间距。
信号:任何把 env() 并进多值 shorthand 的编辑;布局没动过却出现"贴死/错位"。
B7 iOS standalone black-translucent 下根高度用百分比→底部露系统白条,叠 -webkit-fill-available 反把 Safari 压扁
症状:apple-mobile-web-app-status-bar-style: black-translucent + viewport-fit=cover 后,iPhone 桌面图标启动底部出一条白色安全区空隙;另叠 height: -webkit-fill-available 兜底后 Safari(尤其桌面)整个 app 被压扁挤在顶部。
根因:black-translucent 下 iOS 把视图上移垫到状态栏后,height: 100% 对偏移后的包含块测量、永远矮一截,矮出的部分在底部露出 WKWebView 系统白底;svh/dvh 同基于该包含块救不了。-webkit-fill-available 对 height 的解析在 Safari 不可靠(常远小于视口),且级联靠后反而覆盖正常值。
修法:根元素(html/body)高度用视口单位 100vh,子层百分比继承;不要堆叠 svh/dvh/fill-available。改 status-bar-style meta 后需重新“添加到主屏幕”才生效(iOS 在安装时刻快照该设置)。
信号:切 black-translucent 后底部出现白条;给高度链“加兜底”后 Safari 反而变矮。
B8 移动浏览器 100vh 含浏览器 UI 遮挡区 → 垂直居中内容视觉下沉贴底
症状:安卓 Edge/Chrome、iOS Safari 里主菜单卡片上方留白多、底部几乎贴浏览器底栏;桌面或开发者工具模拟正常。
根因:移动浏览器 100vh = 大视口(含地址栏/底栏遮挡区),margin: auto 对全高居中而可见区更矮,底部留白被遮挡区吃掉;这块遮挡不走 env(safe-area-inset-bottom)(它只管系统手势条),加 inset 救不了。
修法:根元素 height: 100vh; height: 100dvh; 回退链——dvh = 实际可见动态视口(Chrome/Edge 108+、Safari 15.4+),不支持的浏览器声明无效自动落回 vh。standalone 场景另见 B7。
信号:移动端居中“偏了”上松下贴;桌面 devtools 不勾动态视口复现不出。
C. URL 状态 / 撤销重做
C1 分隔符避百分号转义
URLSearchParams 必转 , ; 空格。用 -/_ 做分隔符 + 枚举值缩写(h/c),全程零 %。
C2 写读分离防反馈环
set/clear 不回调订阅者(写方自知);onChange 仅响应外部 URL 变化(popstate)。否则"写入→回读→再写入"死循环。
信号:set 之后 onChange 又触发。
C3 等值 set 必须跳过
按编码后的字符串比较,不是引用/值比较——防历史污染(pushState 多余条目)。
C4 微任务批量写入
同帧多次 set/clear 只 pushState 一次(否则一次操作多条历史)。
C5 差异算法比对"目标列表"
症状:后退/前进"没反应"、源删不掉。
根因:移除时与"场上当前状态"比对 → 永不删除。
修法:与目标列表比对;存活源保留原 id/born(不重播生长动画)。位置容差与 URL 精度同一常量派生(COORD_DECIMALS 半格,state.ts)。
C6 所有重要状态进 URL
纯组件状态刷新即丢。页面视图也要持久化(如 ?view=solutions),优先级规则要确定(view 优先于 level)。
C7 非规范 URL 规范化会多一条历史
挂载时初始应用用 silent(不回写 URL)。
C8 iOS bfcache 后退 popstate 不可靠
pageshow 兜底重对齐;幂等——URL 未变则 sync 无变化零开销。
C9 新建/替换 URL 时先复制当前参数,只动目标键
症状:从 dev 参考解按钮带 ?dev=1 进入关卡,dev 消失(dev 模式被关)。
根因:旧 solutionUrl 从零拼 ?lv=..&src=..——其他状态全丢(现改由 urlState 键级写,不复发,历史教训保留)。
修法:new URLSearchParams(base) 复制当前查询参数,仅 set/delete 目标键(与 urlState.flush 同构);被跳转的"页面视图"键(如 v)须显式删,因其优先于目标视图。
C11 appendChild 重挂子树清空 textarea 原生撤销栈
症状:dev 关卡编辑器点「确认生效」后(或放源/切速率等任何触发 app 重渲染的操作后),编辑框撤销/重做失效、滚动回顶,但文本内容还在。
根因:updated() 里每次渲染后无条件 appendChild(panel) 挂 dev 面板——对已挂载节点 append 仍触发 detach/reattach(实测纯原位 no-op 也中招),子树内 <textarea> 的原生撤销栈与 scrollTop 被清空;元素本身未重建,故值/展开态保留,只有原生编辑态死掉。注释「appendChild 幂等」只对了节点不重建这一半。
修法:挂载前守卫 if (root && panel.parentNode !== root) 才 append;hud 重建/面板销毁后 parentNode 变化仍能正常重挂。
信号:任何「每次更新都 appendChild 一次」的幂等式挂载;被挂子树内含表单控件。
信号:手写拼 URL 字符串、链接 href 不含当前其他参数。
C10 返回判定别用 history.length:pushState 带应用标记
症状:直达链接/新标签页进子页面点"返回"跳到别的网站或空白页;window.history.length > 1 时 history.back() 会离开本站(length 是整个会话栈,含外部站点)。
修法:应用内 pushState 统一带 { sf: true }(url-state 唯一写入点),replaceState 保留当前条目标记(应用条目不丢、文档条目不被污染);返回按钮按 window.history.state?.sf 决定 back() 还是回首页。
信号:任何"返回上一页"按钮;手写 history.back() 或依赖 length 的判断。
D. 渲染性能(WebGL)
D1 移动端 iOS Canvas 2D 是 CPU 栅格化
每帧 createRadialGradient 既贵又有累积风险 → 渐变烘焙成精灵位图,每帧只 drawImage。
D2 渲染必须节流
120Hz 屏 rAF 以 120Hz 触发,但模拟只步进 60Hz——只在"有模拟步进的帧"渲染,否则双倍负载渐进掉帧。倍速(GameLoop.setRate)下每帧都步进,渲染再封顶 60Hz(距上次渲染 ≥ SIM_DT 才画)——高速率只放大 tick 成本,不放大渲染负载。
D3 批量描边替代逐段提交
按透明度/温度等分桶 Path2D,每桶一次 stroke。移动端从最多 ~150 次路径提交降到个位数。
信号:每帧路径提交数 = 元素数 × 段数。
D4 热路径零分配
每帧 {x:0,y:0} 之类的临时对象 → 复用字段。采样临时量用共享对象。
D5 ResizeObserver 抖动/循环
尺寸未变则跳过 resize,否则画布每帧重建(iOS 已知坑)。
D6 自适应降级要防误触发
帧开销 EMA(平滑 0.95)+ 慢帧计数(如持续 150 帧超 13ms 才降级)——偶发卡顿不降级;只降 dpr 档(粒子数恒定全平台视觉一致,粒子是核心视觉不可降)。
成本口径 = 纯主线程渲染耗时:模拟迁 worker 后其 tick 耗时与主线程帧预算无关,计入会误触降级——实测 worker tick 28ms+渲染 6ms 恒超 13ms 预算,150 帧(≈5s)后触发一次 dpr 降级 → fit(force) 强制 resize+地形顶点批全量重烘焙 ≈ 80ms 卡顿(LoAF 定位:单个 rAF 回调 77ms),每局必现一次。修法:governor 只喂主线程渲染耗时;fit 加 w/h/dpr 幂等守卫(无实际变化跳过 resize),dpr=1 设备降级也不白卡。
倍速下帧预算按速率放大(预算 × rate):倍速慢帧是用户主动选择,慢帧是预期而非故障。追赶封顶:单帧最多消化 24 模拟步(≈0.4s 模拟,loop.ts MAX_TICKS_PER_FRAME),暂停回归在 16× 下不会单帧冻结。
D7 Canvas 2D → WebGL1 批量渲染(#7 性能重构的结论与要点)
iOS Safari 的 Canvas 2D 是 CPU 栅格化(D1),逐帧上万段 Path2D 描边是瓶颈;WebGL1 在 iOS 8+/全部 WebView 可用且 GPU 加速,是兼容性最优解(WebGPU 太新;物理数值内核 #20 起另走 WASM·SIMD,见 I1)。落地要点:
- 公共 API 不变:Renderer 的 constructor/resize/toWorld/draw 保持原签名,控制器与 UI 零改动。
- 顶点批
render/batch.ts(纯计算无 DOM,可无头测试)+ render/gl.ts(上下文/着色器/缓冲薄层):整帧一个动态 VBO、一次 drawArrays(TRIANGLES)。
- GL
lineWidth 多平台恒为 1:线宽必须几何化——线段沿法线展开为四边形(stroke()),别指望 gl.lineWidth。
- 逐顶点颜色取代分桶:透明度/颜色不再离散分桶(Canvas 的 strokeStyle 状态机所迫),每段直接带精确 RGBA,一次提交。
- 径向渐变 = 扇形逐顶点插值:中心色→边缘色线性插值即等价两端色标的 createRadialGradient,免每帧建渐变与精灵烘焙。
- 顶点缓冲是 float32:无头测试断言用 toBeCloseTo(容差 1e-5),别用 toEqual 精确比较。
- 上下文回收:iOS 内存压力会销毁 WebGL 上下文——
webglcontextlost 要 preventDefault,webglcontextrestored 重建程序/缓冲。
- 混合:
SRC_ALPHA / ONE_MINUS_SRC_ALPHA(非预乘),与 Canvas rgba 语义一致;alpha:false 画布不透明,天空由场景自铺满。
- 静态背景烘焙进离屏纹理(FBO):仅 resize/上下文恢复后重烘焙,动态层每帧重建——烘焙失败要保留脏标记重试,别清掉后静默空背景(D10)。
D9 webglcontextrestored 重建失败会静默白屏 + 重复恢复泄漏 GPU 对象
症状:iOS 内存压力回收上下文后画面空白;或多次恢复后显存持续上涨。
根因:restored 回调里重建程序/缓冲,但 (1) 重建失败(编译/链接错误)时静默早退,program/buffer 仍是已随上下文销毁的旧对象,draw 继续误用 → 白屏且无重试路径;(2) 每次重建都不删旧 shader/program/buffer → 重复恢复反复泄漏。
修法:init() 开头 dispose() 删旧对象(恢复后旧对象本已失效);失败路径删除已创建对象并置空指针返回 false,restored 回调据返回值报错——draw 检查 !program 跳过,不碰失效对象;shader 在 link 成功后即可 delete。
信号:上下文恢复相关代码出现"早退不清资源"或"重试不清理旧对象"。
D10 上下文恢复后烘焙背景丢失(地面/天空缺失、动态层正常)
症状:偶现"地面/天空没了",飞机/粒子还在动;刷新或改窗口大小即恢复。
根因:restored 只重建 program/缓冲,离屏背景纹理/FBO 随上下文销毁后没有重建,且 Renderer 的 bgDirty 为 false 不会重烘焙——draw 落入"背景未就绪"兜底清屏,只画动态层。
修法:restored 里 init 成功后立即 resizeBg() 重建纹理/FBO 并置 bgStale,Renderer 烘焙条件为 bgDirty || gl.bgStale;bakeBg 检查 checkFramebufferStatus,不完整则重建 FBO 并保留脏标记下帧重试。
2026-08 续坑(原修法仍有洞):bakeBg 失败路径(FBO 瞬态不完整/纹理分配失败)返回后,Renderer 无条件清掉了 bgDirty/bgStale——重建出的空纹理或兜底清屏会一直顶到下次 resize/上下文事件,即"刷新或改窗口大小才恢复"的偶现白底。修法三件套:(1) bakeBg/resizeBg 返回 boolean,烘焙失败时调用方保留脏标记,且 bakeBg 失败路径就地重建 FBO/纹理,下一帧的检查即对新建对象进行;(2) resizeBg 里 createTexture/createFramebuffer 返回 null(显存压力)时指针置 null 返回 false,走同一重试链;(3) 烘焙条件加 !gl.bgReady,纹理缺失即使无脏标记也强制进块自愈。浏览器实测:注入纹理丢失后 ~2 帧内自动恢复。
信号:离屏纹理/缓存的资源在"上下文恢复"路径没有重建入口;烘焙失败路径出现"无条件清脏标记"。
D8 淡出/裁剪的早退别跳过物体本体
旧 drawPlane 在影子淡出高度以上直接 return——连飞机本体都不画,高空飞机凭空消失(纸飞机阴影渲染已移除,此例退为历史教训)。任何"某效果随条件淡出"的代码,先确认早退范围不含主体绘制。重构渲染时优先审这类 early-return。
D11 模块级初始化抛错 → 整包白屏(如关卡解析失败)
症状:改坏一个关卡 JSON(如 "r": 0)后刷新页面直接白屏,console 是模块求值时的 Uncaught Error: 关卡校验失败…。
根因:levels.ts 在模块顶层 LEVEL_TEXTS.map(parseLevelText)——任一条解析抛错,整个 bundle 求值失败,连 sf-app 都注册不了,任何错误 UI 都没有机会渲染。
修法:逐项 try/catch 容错加载(坏关卡进 LEVEL_ERRORS 清单,模块永不抛);UI 在标题页渲染告警卡(role="alert" 红底小卡列出错误原文);依赖首关的字段初始化(如 hud 的预算初值)用 ?. + ?? 0 兜底,startGame 找不到关卡直接 return。
信号:任何"模块顶层立即执行解析/编译/IO"的代码(关卡、JSON、wasm 初始化)——一律逐项容错 + 错误清单外显,白屏是最大的鲁棒性失败。
D12 顶点批容量整批预算检查 → 地形闪烁/整体消失
症状:地形渲染闪烁,过一会儿整体消失;重置无效、刷新页面恢复(粒子越多越易触发)。
根因:bTerrainDraw 用「视域格数 × 最坏每格顶点」的粗预算与剩余容量比较,超了整批丢弃。同帧先入批的动态图元(示踪粒子饱和后 ~7 万顶点)逐帧波动,把预算顶到临界——临界帧闪烁、饱和后永久丢弃;restart 不重建粒子群故不恢复,刷新重建一切故恢复。
修法:整批预算改为逐图元/逐格检查(每格至多 12 顶点,count + 12 > CAPACITY 即停),容量按最坏帧总量(地形+示踪+其余)留余量。容量临界时优雅截断,绝不整批丢弃。
信号:任何「按最坏估计一次性决定整块画不画」的预算检查,且同帧有先入批的波动性图元。
D13 拖尾逐段独立图元 + 逐段恒定 alpha → 透明度阶梯突变与接头缺口
症状:拖尾肉眼可见一节节色带(透明度阶梯),急弯处有细小缺口;整体淡出时一节节跳变而非连续融化。
根因:逐段 stroke 每段一个恒定 alpha(采样点淡出值),相邻段 alpha 差直接台阶化;平头线段端帽在拐弯处不闭合(无 miter)。
修法:单条 polylineFade(miter 接头)+ 逐点线性 alpha;尾端空间淡出与时间淡出取小,最旧端 alpha 恒 0。
信号:轨迹/拖尾类渲染用「循环里逐段画」的写法——凡透明度沿路径变化,必用逐点 alpha 折线带。
D14 视觉粒子 PRNG 用 Math.random → 每次刷新画面不一样
症状:同一关卡每次刷新粒子初始分布/云布局不同,玩家感知「每局不一样」。
修法:种子由关卡 id 派生(mulberry32/imul 混洗),同关逐位可复现;种子参数由调用方显式传入(不默认随机)。
信号:纯视觉层的 Math.random()/Date.now() 播种——装饰物随机必须是「可复现的随机」。
D15 半透明图元自叠加 → SRC_ALPHA 双重混合使端头/接头发深
症状:圆头线段两端比中段深、虚线每节两头深、环接头一圈暗点、粒子头部与拖尾重叠处一团深。
根因:SRC_ALPHA/ONE_MINUS_SRC_ALPHA 下同一形状被多个图元覆盖即叠乘:整圆盘端帽压在线段带上、头部圆盘压在拖尾线带上、逐段 stroke 在接头互叠。
修法:零重叠构型——端帽用朝外半圆盘(π 扇,恰补线段带延长区)、闭环用 miter 折线、拖尾头部顶点 alpha=0 让头部圆盘独享混合。跨粒子互叠是半透明 painter 固有成本,靠低 alpha 缓解,不做 OIT。
信号:「多个半透明图元拼一个形状」的写法——先问拼块之间是否零重叠。
D16 程序化 GLSL 形状的噪声调制把可见轮廓推出四边形 → 直边裁切感
症状:片元噪声塑形的软体(云/光晕)边缘出现水平/垂直直边,像被矩形切了一刀。
根因:噪声项在四边形边界处不为零:基椭圆在边界 d<1 时,最坏噪声(−幅值)把 d 拉回阈值内,alpha 在 quad 边仍 >0,被四边形硬切。
修法:基椭圆缩放到四边形边界处 d ≥ 阈值+噪声幅值(云:length(q*vec2(1.35,1.5)) 配 ±0.4 噪声),quad 尺寸按占空比反算包围盒;宿主只发四边形,形状全在片元。
信号:「quad + 片元距离场」写法——先验边界最坏噪声下 alpha 是否归零。
D17 渲染侧滞后状态对模拟时钟差分 → 重置负 dt 炸状态,旗面永久消失
症状:抵达目标后按重置,旗面消失(旗杆/套筒还在),再重置也不恢复,只有刷新页面好;跑的时间越长越易中。
根因:restart 将 sim.time 归零,而 Renderer 跨重置持久:首帧 dt=0−旧时为负,k=1−exp(−dt·rate) 变负巨值,滞后状态逐帧乘巨幅交替变号,约十帧溢到 Inf,dx=Inf/Inf=NaN,顶点 NaN 被 GL 丢弃;NaN 是吸收态,重置不重建 Renderer 故永久。
修法:对模拟时钟差分的 dt 一律夹 ≥0(dt=0 → k=0 自然不动,状态保留上一局收敛值无缝接续)。
信号:「Renderer 持有状态 + sim.time 差分」组合——先问时钟能否回退(restart/切关/回放)。
D18 归一化方向向量 + 硬阈值 → 过零点宽度突跳,旗面「啪」消失再弹出
症状:潮汐关风向反转时,旗面从宽约 1 单位瞬间跳成 0(消失一瞬),再在另一侧弹出,不丝滑。
根因:方向取单位向量(sx/u)使宽度与 u 解耦,u>阈值时宽≈len,u<阈值硬置 0——阈值两侧不连续。
修法:方向向量 = 单位方向 × min(1, u/FULL),u<FULL 退化为 sx/FULL,随 sx 线性过零(宽度连续收拢→反向展开);除法守 u>1e-4。
信号:任何「向量归一化 + u>eps 硬阈值」的朝向渲染——先问该量能否过零,过零处是否连续。
D19 顶点批循环守卫单位混用:只画第一朵可见云(2026-08-14 实踩)
症状:动态实例批(云)出现「淡出后另一朵满不透明度异地闪现」的瞬移观感,同批可见实例数恒为 1;还叠加"濒死云与新生云在阈值两侧横跳"的频闪。
根因:for (i < count && n < CAP) 的 n 是浮点写入计数(每实例 36 个),CAP 却是实例数——写完第一个可见实例 n=36 即越限提前结束;隐形实例 continue 后 n 不变,又让下一个可见实例顶上 → 实例间硬切换。守卫是"容量钳"防御性添加时单位写错的(CR 引入回归)。
修法:钳制必须同单位(n < CAP × 每实例浮点数);填充逻辑抽纯函数(app/render/cloud-batch.ts)由白盒测试钉死"全可见 = 全写入、隐形不占容量"。
信号:批内多实例却只见一个、且消失/出现在不同实例间横跳——先查循环守卫单位;任何"n 数的是 A 却和 B 比"的循环都照此查。
A10 shadow DOM 内 pointerdown 的 e.target 被重定向成宿主
症状:自定义元素内部"从某子元素按下"的判定失效——要么什么都拖不动,要么按钮/输入框一按就触发拖动(dev 面板拖拽冲突实测,2026-08)。
根因:监听器绑在宿主上、目标在 shadow 树内时,事件跨过边界 e.target 会被重定向成宿主,e.target.closest('.head') 永远不中(反之排除检查 closest('button, textarea') 也永远不中)。
修法:判定真实命中元素用 e.composedPath()[0](shadow 树内原目标),再 closest()。
信号:宿主元素监听 pointer 事件、要区分"按在哪个内部控件上"的代码——一律走 composedPath()[0]。
A11 Lit 组件字段缺 @state:赋值不触发重渲染,交互"明显延迟"
症状:按钮点击后要等 1~2 秒才看到画面变化(dev 面板展开/收起实测,2026-08);分拆独立组件后甚至完全不更新。
根因:expanded/editorText 等交互状态是普通类字段,this.expanded = true 不触发 Lit 更新——旧版靠周期 refresh() 的 requestUpdate()"顺带"重绘(90 帧 ≈ 1.5s 一次),于是交互总有 1.5s 级延迟;新版没有周期刷新就永不重绘。
修法:所有驱动模板的状态字段一律 @state()(含外部直接赋值读取的,如 paused)。
信号:组件内有"点了没反应/慢半拍"的交互字段——查它是不是普通字段;组件依赖外部周期性 requestUpdate 才更新。
A12 Lit CSS 模板里 // 不是注释:整条规则静默失效
症状:给 static styles 某条 CSS 声明加 // 注释 后该规则完全不生效(如宽度规则失效、盒子缩回内容宽),无任何报错;:host 里混入更隐蔽(整块 :host 声明可能被废)。
根因:css 标签模板把内容原样拼进 <style>,CSS 里 // 是非法词法(CSS 注释只有 /* */),解析器按错误恢复规则吞掉后续声明。
修法:CSS 模板里注释一律 /* */;// 只允许写在模板外的 TS 代码处。
信号:改动只加/改了注释、某样式却失效——先查注释写法;dev 面板宽度类规则尤其常见。
A13 嵌套 html 模板做 SVG 子内容:图标整片空白(2026-08 全端实踩)
症状:图标/内联 SVG 尺寸位置全对、就是不绘制;同内容独立 svg 文件正常;DevTools 里编辑该元素 html 强制重解析后突然出现(所有浏览器一致)。
根因:外层 lit html 模板内再嵌套 html 模板做 SVG 子内容时,内层经普通 <template> 在 HTML 上下文解析,子元素落 HTML 命名空间——SVG 渲染器拒绝绘制 HTML 命名空间的图形元素。
修法:SVG 内层一律用 lit 的 svg tag(解析时以 <svg> 包装再解包,子元素落 SVG 命名空间),外层保留 html 全量 svg 字面量;或 unsafeSVG。
验证:全树 namespaceURI 无 badNamespaces + 像素暗度与原版逐像素一致(CDP/MCP 数值化探测,见 H1)。
A14 半透明面直铺插画/动态背景不带模糊 → 文字混入背景不可读
症状:页签/提示条等玻璃面在插画封面背景上文字发虚,纯渐变背景下勉强可读——同组件在不同页面可读性不一致。
根因:半透明底(rgba 低不透明度)的可读性全靠 backdrop 模糊抹平背景对比;页面背景是插画(artBg)而非纯渐变时对比波动大得多,无模糊直接混色。
修法:半透明面 + backdrop-filter: var(--blur-glass)(含 -webkit- 前缀)成对出现;唯一豁免 = 嵌套在已模糊容器内(子面只做浅叠加不重复模糊)。新增直铺背景的面时逐一核对,别只抄相邻面的 background。
E. 手势 / 移动端
E1 touch-action: none + user-scalable=no
否则长按弹系统菜单、双击缩放。
2026-08 续坑:视口 meta 已 user-scalable=no,但用户开启系统辅助缩放(Settings → 缩放)时该 meta 被忽略,iOS 双击按钮仍会放大页面。修法:在根元素(sf-app)设 touch-action: manipulation——祖先值约束全部后代(canvas 自身 none 取更严交集,拖尾手势不受影响),双击放大被禁、滚动保留。Android 无此问题。
E2 右键与左键冲突
pointerdown 只处理 button === 0(右键若放行会先走热源 tap 流程),右键放冷源走 contextmenu(preventDefault)。
E3 长按判定用定时器 + 位移阈值
长按达阈值即确认(380ms),位移超过 slop(14px)取消;pointercancel 清理所有轨道与定时器。
E4 iOS 聚焦字号 <16px 的输入框自动放大页面
症状:iPhone 上点输入框整个页面被放大、布局错位。
根因:iOS Safari 对焦 font-size <16px 的 input/textarea 自动 zoom-in 凑可读字号(user-scalable=no 拦不住)。
修法:输入框 font-size: max(1rem, 16px)——根字号低于 16px 时钉死下限,其余场景随 rem 缩放。
E5 按钮焦点与全局快捷键冲突
症状:点过 HUD 按钮后 Space 变成再点该按钮而非暂停;或图省事把 BUTTON 列入输入源全屏蔽,导致快捷键整体失效。
根因:UA 用 Space/Enter 激活聚焦按钮,与全局快捷键双触发;但焦点留在按钮上只该影响 Space。
修法:快捷键层仅 Space 门控按钮焦点(让位原生激活);另在 window 捕获 click 对指针点击的按钮 blur() 归位焦点(MouseEvent.detail !== 0 判指针;键盘激活的 click detail === 0 不 blur,键盘用户焦点流不受损)。
E6 弹窗 Esc 拦截必须 window 捕获阶段 + stopImmediatePropagation
症状:弹窗开着按 Esc 触发全局退关/返回,或两个弹窗同时被关。
根因:焦点在 body 时事件不经弹窗 DOM,挂 scrim 的 keydown 收不到;stopPropagation 不阻断同一 window 节点上的其他捕获监听器,多弹窗同时注册会连环触发。
修法:window.addEventListener('keydown', fn, true) + e.stopImmediatePropagation();监听随组件 connect/disconnect 成对装卸。
E7 触摸长按与原生 contextmenu 双触发
症状:手机长按放冷源成功,但手指未及时抬起时刚放的源立刻被删;桌面右键一切正常。
根因:移动端无右键,长按到 UA 阈值(~500ms)会再派发原生 contextmenu——若把它当右键语义处理(命中即删),恰好命中长按刚放下的源。长按计时(380ms)必然早于原生事件,冲突不可避。
修法:contextmenu 里 pointerType === 'touch' 直接拦截(触摸冷源恒走长按手势);旧引擎 touch contextmenu 可能无 pointerType,兼容兜底 = 触摸长按确认后记时间戳,窗口内的无类型 contextmenu 一并吞掉。
F. 音频(WebAudio)
F1 iOS 必须用户手势中创建/恢复 AudioContext
pointerdown 时 unlock(ctx.resume())。BGM(HTMLAudioElement)同受自动播放策略约束:fb.unlock() 单点布防(feedback.ts 的 unlockAudio),首次手势同时解锁 sfx 与 bgm——bgm 若只在 app 构造时 play() 一次,非手势上下文被拒后无人重试会永久沉默(2026-08 统一化)。
F2 后台挂起 + 恢复可见立即恢复
visibilitychange:hidden → suspend;visible 且 suspended → resume(不依赖下次触摸,否则切回页面无声)。
F3 WebKit 音频图持有已完成节点
一次性音源播完后显式断开整条节点链(ended 事件),否则长会话累积 → GC 压力与掉帧。
G. 物理模拟 / 测试
G1 时间累积浮点误差
t += 1/60 累积后 Math.floor(t) === t 在整数秒处失效(只命中一次)。用整数步数:step % 60 === 0。
G2 确定性模拟可作测试断言(#27 起注册解不设自动回归)
无随机项的模拟,通关时刻确定——历史上 solutions.test.ts 曾断言"注册解可通关且通关时间 ±2s";#25/#27 起降级为只查"有解且不超预算";解法不再随关卡文件发布后该测试已删除(通关记录由玩家产生,progress.ts 与关卡 hash 绑定)。
G3 vitest 长模拟必须显式传超时
test('...', () => {...}, 30000)——默认 5s 必挂。
G4 高时间复杂度的搜索算法先评估
运行前估算复杂度与耗时(如 13³ 网格 × 45s 模拟 ≈ 数小时——先算再跑)。用有界候选集 + 早退。
G5 批量"删注释"编辑事故
用整块替换删注释时,oldString 稍不精确就把常量/方法本体一起删掉。每次批量编辑后立即 tsc -b 验证;删常量前先 rg 确认其引用位置。
G6 两段式搜索:粗筛 dt 胜点必须精验(#10 关卡调参实测)
症状:dt=1/30 粗筛出的胜点在 dt=1/60 精验时 -1(不通关)——粗筛全假阳性,白跑数小时;反过来,已知参考解在粗筛下也可能假阴性(实测 L1 参考解粗筛 60s 不通关,精筛 22.2s 通关)。
根因:流体/刚体对 dt 敏感,粗 dt 是"另一套物理",能造出粗筛独有捷径(也能丢掉精筛独有的解法)。
修法:调参统一两段式——粗筛(dt=1/30、cap 25s、早退)只用于砍掉明显不行的组合;所有候选必须 dt=1/60 精验(浏览器固定步长 SIM_DT=1/60,精验即与真机一致)。#10 验收后改为搜索直接跑精筛(run-level.ts --solve 的 worker 固定 dt=1/60、cap 35s;#25 起贴地可被风重新带飞,不设贴地早退),彻底消除 dt 假象;搜索前先估复杂度(单次评估 ≈ 1–2s),加墙上时间预算 + 每代打印进度,超预算即止。
信号:任何"搜索找到解但测试不过/真机不通"的差异,先查 dt。
G7 参考解的扰动鲁棒性抽查(可玩性代理指标)
症状:参考答案能通关(验证绿),但玩家手指偏 1 格就完全不通关——参考解按钮给的是刀尖路径。
根因:时间最优解往往贴着站点圆边缘擦过,轨迹对源位置极敏感。
修法:把参考解每个源做 ±1 单位(8 方向)扰动,统计仍通关比例。教学关建议 ≥6/8;全挂则放大站点圆(r 9-11)或换布局。注意:本作参考解本质是"速通线",2/8~8/8 都正常,只要不是"除了精确点全废"。
信号:站点圆 r<8 且解法只擦边通过 → 先扩圆再谈速度。
G8 贴地滑行与飞行同等计数(#25 起,旧"滑行不算过关"不变量已废)
症状:贴地滑进抵达圆即过关;贴地飞机在风下可被重新带飞,怀疑与旧认知不符。
根因:旧物理刻意让贴地飞机难再起飞、且"贴地滑进目标圈不算过关";#25 调整后抵达圆内滑行与飞行同等计数(AGENTS.md 玩法不变量),地面边界层参数同时放宽。
修法:挂机防线靠关卡设计——新关卡用 run-level.ts --sim 150 自查挂机轨迹不穿过任何抵达圆;需要"必须飞行才过"的关卡,用地形(目标在崖/墙之后)而非通关判定实现。
信号:老文档/老测试声称"贴地通关不算数"→ 已失效。
G9 逐元素 expect 断言开销放大
症状:对百万级采样逐个 expect(Number.isFinite(v)).toBe(true),断言本身耗时数秒,远超被测渲染本身。
修法:循环内用标志位聚合(allFinite/peak/计数),循环外一次性 expect。同类:重复的全量渲染/烘焙应在 describe 作用域做一次共享复用。
G10 模拟循环内重复创建重资源
症状:for (i...) stepBody(body, makeCalmFluid(), ...)——每步新建流体网格,1200 次分配把 100ms 的模拟撑到 3s。
修法:静态资源(流体/引擎/画布)循环外建一次复用;除非测试目的正是重建本身。
G11 姿态不是纯表现层:它经 vertexRestY 反馈位置
症状:改姿态基准坡度(中心差分→地形弦)后,停稳点沿坡缓慢蠕爬——姿态变 → 最低触顶点变 → snap 中心高度变,逐帧泵浦。
修法:姿态类改动必须跑长时段挂机轨迹探针验证停稳位置/姿态收敛,不能只看单帧姿态快照。另一变体:候选目标"取距当前姿态最近者"时,已收敛的旧目标距离=0 永远压过新候选——新目标应按"同侧升级"替换旧目标(如双点接地解替换底边贴坡角),再二选一。
G12 网格域加偏移后,世界↔网格换算必须处处带 origin
症状:流体域外扩边距(origin 偏移)后,空中大片被误标固体、探测点采样恒 0——多个测试同时挂且症状互相无关。
根因:buildSolidMask 把格中心索引 (i+0.5)*cell 直接当地面函数的世界坐标喂——偏移后掩码整体错位。凡是"格索引 → 世界坐标"的点(掩码、采样、渲染)都要减 origin;凡是"世界 → 格索引"的点都要加 origin。
修法:引入域偏移时全仓 grep \+ 0.5) \* cell 与 / cell 清点所有换算点;无头测试用 margin:0 退化为旧几何隔离验证。
信号:改网格几何后多个不相干测试同挂 → 先查坐标换算链。
G13 装饰物体的回收边界必须对齐可视区,重生恒在场内
症状:云被吹出可视区后长时间看不到云。
根因:回收边界(±30)远大于视域溢绘,云停在「已出屏但仍存活」的空窗区(强上升流平衡点可达 homeY 上方几十单位)永不回收;且部分重生落在场外带,同样长期不可见。
修法:回收边界收紧到 ≈ letterbox 溢绘(±12);重生恒在地图内(出场即可见),删掉场外出生带;下沉轮换交给 SDF 距地表判定,删掉累计 descent 冗余通道。
信号:任何「出屏后靠更大边界慢慢回收」的装饰物体——边界必须贴着可视区,重生必须回到玩家看得见的地方。
G14 视觉粒子槽位复用不得强夺活粒子:自然死亡再转生
症状:热源旁粒子轨迹时不时整条瞬消(alpha 突变)。
根因:羽流注入随机挑一个粒子强夺其槽位:被夺者正可见(env≈1),轨迹与头部直接清零——肉眼可见的消失爆点。
修法:改为自然死亡时按概率转生为羽流(粒子本已淡出完毕 env→0,无突变);稳态密度 ≈ 死亡率×转生概率×短寿命,与原注入同量级。
信号:粒子系统「随机挑一个现有粒子改造成新效果」的写法——先问被挑中者当前是否可见。
G15 装饰物须自然寿命轮换 + 生成间距拒绝采样
症状:云在地图边缘停滞很久、多朵云堆积同一区域、新云贴着旧云生成。
根因:被动随风 + 仅靠出界回收:弱风/潮汐风下云在界内振荡永不触发回收;重生位置独立随机,无间距约束。
修法:自然寿命取代人工年龄——云累积下降超限消散(下沉绝热增温、云滴蒸发)+ 出图回收,init 错开累积量免同刻轮换;重生拒绝采样与已有云保持间距(半径和×1.15,重试 6 次兜底接受)。
信号:任何「只靠出界回收」的被动装饰物——问一句:风停了/风振荡时它会不会永远停在那。
G16 sponge 带风采样直接用于平流 → 装饰物堆积地图边界
症状:云漂出地图后在 letterbox 溢绘带失速堆积,不再回收(移除人工寿命后暴露)。
根因:sponge 吸收层是流体域的数值边界 artifact,带内风采样衰减≈0;装饰物进带失去驱动力,而销毁边界在更外永远走不到。
修法:图外不采 sponge 带,改用远场风(天空代表点采样)平流;图内仍用当地风。出生漂移兜底潮汐反转瞬间的≈0 风。
信号:任何「风采样平流」物体跨出地图边界——先问域外采样值是什么。
G17 追帧 accumulator 欠账无上限 → 倍速降级延迟数秒生效
症状:16× 低帧率运行后切回 1×,游戏仍满转快转、帧率仍低,持续数秒才突然恢复——倍速切换像延迟生效。
根因:定步长循环 acc 每帧加 frameDt×rate、单帧消费封顶 24 tick;高倍速低帧率时加的多于消费的,欠账无限增长。切回低倍速后欠账仍在,每帧按封顶还债(sim 时间远超实时 = 看似仍高倍速),还清才恢复。
修法:欠账封顶 = 单帧封顶量(MAX_TICKS_PER_FRAME×SIM_DT),超出丢时间(时间膨胀)不追帧;降级切换下一帧即回落。
信号:「追赶/补帧/欠账」机制——先问欠账有无上限、速率降级时谁来还。
G18 关卡扰动鲁棒性:贴地源结构上限 5/8,多段接力复合脆弱
症状:--verify --robust 远低于 75%:贴地源(y≈地表−0.7)扰动 y+1 埋进地形直接失效,多源接力解每个源都刀刃。
根因:扰动是每源 ±1 八邻域;贴地源三个邻位进实体结构性判负,上限 5/8;多段空中接力每段容错窄,逐段相乘。
修法:高鲁棒解形态 = 环境风承担主运输 + 源只做粗调(参考 zhong-bai);目标圆加大(r 10-14);GA 用 --min-dy 8 把搜索域抬离地面;接受接力型关卡鲁棒性低于单源关(既有登记的 ni-lu 亦 0%)。
信号:新关卡定稿前必跑 --robust;贴地源占解主体时先预期 ≤ 62%。
G19 流体内核"跳过固体格"优化:中间状态也有逐位契约(2026-08 空气表优化实踩)
症状:空气索引表优化后 golden 两场景挂(均有 ambient.temp≠0),报"u 场 hash 不符";ambient.temp=0 的场景全过。
根因:apply_buoyancy 旧实现对固体格也施加浮力(无 solid 检查,v[solid]=−k·a_t),随后 copy_fields 把它拷进 v0,平流回溯采样会读到固体位置的 v0——这是合法中间状态,不是垃圾。空气表跳过固体格直接改了这个中间态。
修法:buoyancy 恢复全内域遍历(唯一消费固体 v 的环节);其余扫描(advect/div/GS/grad)固体格值为恒 0 且被 enforce_boundary 清零,跳过安全。GS 红黑相位内单元格独立 → 相位内遍历顺序可改(空气表行主序与旧逐行扫描逐位等价)。
信号:改全网格扫描前逐个盘问"这个数组的固体格位置的值会被谁读"——中间态契约(不是最终态)才是坑。
G20 SIMD 车道打包:"相距 2 格"不是"内存连续"(2026-08 GS 双格优化实踩)
症状:GS 双格 SIMD 车道 0 逐位正确、车道 1 错得离谱(非 1 ulp);逐来源检查后发现竖邻居(p[a−nx] 与 p[a+2−nx])取错。
根因:格 a 与格 a+2 的竖邻居索引差 2 → 字节地址差 8,不是连续 f32;用 v128.load32x2_u(加载 2 个连续 f32)取到了 p[a−nx]、p[a−nx+1]。横邻居同理(a−1 与 a+1 差 8 字节)——正确做法是 4 宽载入 + i8x16_shuffle 抽 lane0/lane2 再 f64x2_promote_low_f32x4。
修法:非连续对一律 4 宽载入 + shuffle;仅当真连续(字节地址紧邻)才用 load32x2_u。差分测试用独立伪随机场把两个格都标量算一遍逐位对比(曾因只标量算 lane0 格、把未算的 lane1 原值当基线,误判 SIMD 错)。
信号:SIMD 车道间结果一个对一个错 → 先查非连续元素是否被当成连续载入。
G21 主→worker 无背压 → 高倍速交互秒级延迟(2026-08 实测)
症状:高倍速(4×/8×/16×)"运行中"点击暂停或增删冷热源,动作延迟数秒才生效;从暂停恢复则无延迟;页面本身不卡(60fps)。
根因:GameLoop 每帧把 rate×tick 条 tick 消息发往 worker;worker 逐条同步处理(每条 ≈ 流体内核耗时)。设备内核吞吐低于倍速要求时,tick 在 worker 消息队列无界积压,交互消息(place/remove/pause)FIFO 排在积压之后——延迟 = 积压 × 单 tick 耗时,随"运行中"时长增长。暂停时 tick 走冻结空转路径(亚毫秒),队列恒空 → 恢复无延迟。页面流畅是因为 MAX_ACC 丢时间(时间膨胀)+ 渲染在主线程。
修法(已落地):controller 用帧快照回执(每 tick 1:1)计数在途 tick,达 MAX_TICKS_PER_FRAME(24,loop.ts 导出)即停发——积压上界 24 条,交互延迟上界 = 24 × 单 tick 耗时(Android ≈100-150ms);停发时 acc 照常扣减 = 时间膨胀,与既有 MAX_ACC 语义同源,能跟上的设备零影响。
信号:「倍速下交互卡、画面不卡」「暂停有延迟、恢复无延迟」——先查消息积压(模拟速率 vs 墙钟),别查渲染。
G22 跨线程传参纪律:传输量要与消费量成量级对应(2026-08 实测,方案已回滚)
症状:帧率不降,但主线程 usedJSHeapSize 秒级锯齿(6↔39MB);肉眼无卡顿,真机无感知差异。
根因:worker 把 5 个全场 + 全部示踪缓冲逐 tick 全量 transfer(919KB × 60Hz ≈ 54MB/s),而主线程对场的实际消费仅 ~400 次双线性采样(12µs)——传输量大消费量两个量级;transfer 虽免复制,但每帧被 detach 的 ~1MB 在主线程成 GC 垃圾。
方案与回滚:曾实施 worker 预计算(着色批直写内核记录布局 + 旗面风留 worker 侧,919KB→~130KB,headless 实测主线程 GC 锯齿减半、tick/batch 持平);真机无明显提升,已回滚(d0072db)——现代移动端 GC 对 detach 缓冲的回收开销远小于理论值,为此付出的"worker 耦合批次记录布局 + 双份着色逻辑"代价不划算。
信号:任何跨线程逐帧传输先算「每秒字节数 ÷ 接收方消费量」;但先真机验证再决定是否重构——headless 的 GC 指标不等于真机体验。
H. 布局调试方法论(实测有效)
H1 数值化探测替代目测截图
注入探针脚本读 getComputedStyle + offsetWidth + getBoundingClientRect + 祖先 transform,多尺寸(320–1920)headless Chrome 验证。
隔离复现不可信:布局 bug 往往只在真实内容量级下触发(如 A4 百分比循环),必须用真实页面测。
H2 三数值不一致 → 先查 transform/zoom(A1);样式对但布局错 → 查 box-sizing(A2)
I. 双环境工具链 / 浏览器自动化(实测)
I1 加速技术先跑真机基准再定默认(wasm 血泪教训,2026-08)
实测:MoonBit 逐位一致的 wasm 求解器在所有实测平台都比 JS 慢——iPhone Safari +48%、macOS Safari +39%、Chrome +142%(JIT 引擎的 JS typed-array 数值循环已接近原生,wasm 的调用/转换开销是负资产),该套代码已移除。但 #20 改用 AssemblyScript 在更大网格/更重内核上重新实测:WASM·SIMD 全面领先(~2-4×),遂定为唯一后端;#21 按老大指示移除全部 JS 流体后端与切换机制(?be=、--backend、bench-backend)。
教训:上任何加速技术(wasm/代码生成/Worker)前,先做真机基准再定默认;结论随时间与实现水平变化,过时基准要重测。当前(2026-08)基准:流体 0.5ms(JS 时代)→ wasm 迁移后更低、倍速帧 16× <12ms。
帧成本画像(2026-08-06 V8 实测,LEVEL_2 101×75 网格、400 粒子):fluid.step 0.57ms/tick · tracers.step 0.08ms · drawTracers JS 循环 0.04ms · 整帧批组装(6.6 万顶点)0.57ms——成本在 wasm 内部 tessellation,不在跨边界(单次边界调用 ≈3ns,每帧 ~2000 次仅 ≈6µs)。剩余 JS 数值循环合计 <0.15ms/帧,暂无有意义的 wasm 迁移目标;再要提速走渲染算法层(降顶点/实例化),别再想"JS→wasm"。
注:bench 工具链(bench.html、scripts/bench*.ts、app/dev/bench-core.ts)已按老大指示移除(2026-08);浏览器验证用 chrome-devtools-mcp 直连(原 CDP 一致性脚本 scripts/browser-consistency.ts 已随之移除)。
I2 vite dev 只绑 IPv6 回环
症状:vite dev 起来了,curl http://127.0.0.1:端口 连接失败(000),localhost 正常。
根因:vite(新版本)只监听 ::1。自动化脚本访问 dev server 一律用 http://localhost:端口。
信号:headless 自动化/curl 探测 127.0.0.1 失败但浏览器手开正常。
I3 bun 的 node:child_process 兼容坑
spawn 的 stdio 传 stream(createWriteStream 等)报 "TODO: stream.Readable stdio";传 Bun.file() 报 "Invalid stdio option"。
- 修法:脚本统一用原生
Bun.spawn([cmd, ...args], { cwd, stdout: 'ignore', stderr: 'ignore' }),cwd 必须显式(vite 从 cwd 找配置/根,cwd 错会 404)。
bunx 会产生孙进程,kill 不掉会占端口——直接跑 node_modules/xxx/bin/xxx.js。
I4 浏览器自动化:chrome-devtools-mcp 直连(原 CDP 脚本已移除)
headless Chrome 自动化现直接用 chrome-devtools-mcp 工具连接(页面快照/JS 求值/网络/追踪),无需自维护 CDP 客户端。原零依赖 CDP 方案(scripts/cdp-client.ts + scripts/browser-consistency.ts)已随其接入移除(2026-08)。
I5 真机基准页的"帧预算"解读
iPhone 上 performance.now() 分辨率 ~1ms:p95 出现整齐的 1.000ms 是量化底噪,不代表真实抖动;看 mean 与整帧构成(倍速帧项)判断瓶颈。iOS Safari 的 fluid JS 比桌面还快(0.49ms)——"移动端更慢"要逐平台实测,别想当然。
I8 JSC 对 wasm SIMD 的误编译与双运行时纪律(2026-08 实测定案,bun 1.4.0 / JSC)
症状(历史):run-level --verify 输出「通关 0.0s · 路程 NaN」、偶发越界 abort;tests/fluid.test.ts 无地形「热源上升风」在 bun 运行时 v≈0.28 而 node/V8 得 v≈6。
根因(2026-08-14 计数器插桩定案):JSC 的 wasm SIMD lowering 对 gs_pair(f64x2 双格打包)的无地形全 bulk 路径生成错误机器码——gs_pair 调用计数为 0(纯标量)时两引擎逐位相同,>0 时 bun 发散;node/V8 恒位正确。地形路径(含 7200 步长时程、含 SIMD)两引擎逐位一致——"run-level 在 bun 下不可靠"的旧结论已被证伪(官方工具 bun 结果与 node 逐位相同)。同类 JSC 回归存在外部先例(WebKit bug 314024)。
修法(已落地):
- 语义层门控:
build_air_lists 末尾 solid_count == 0 时清空 bulk 表——无实体纯空域关卡自动走标量 GS;有地形关卡 SIMD 照常(V8 性能零牺牲)。
- 双运行时常设通道:
bun run test = test:moon + vitest(node/V8 权威基线)+ bun node_modules/vitest/vitest.mjs run(bun/JSC = Safari 代理检测器)——bun 直跑从"禁止"反转为"必跑",Safari 引擎家族的回归当场现形。
红线:SIMD「读同格写入目标」(SOR 类)曾在 JSC 复现另一触发(已回退 plain GS);新增任何 SIMD/FFI 数值路径前必须 node/bun 双运行时对拍 + 位级等价白盒测试。
现状(2026-08-16 复核):门控只挡 gs_pair——buoyancy2 是无条件 SIMD(无地形路径也跑),实测 bun/JSC 下无地形+buoyancy2 全量执行结果正确(tests/fluid.test.ts「热源上升风」bun 通过),但它是 JSC bug 触发面扩大时的唯一裸露点;bun 升级后跑 bun run test 复核。另:moon 的 v128 API 标注 alert_experimental(工具链升级可能破坏 SIMD 代码),由 canary+golden 兜底。
信号:无地形/纯空域场景下 bun 与 node 数值不一致、或 bun 运行套件变红——先查 SIMD 路径是否被门控绕过。
I6 iOS Safari WebGL(ANGLE→Metal)性能要点(2026-08 实测 + WebKit bug 255987)
根因:iOS 15.4 起 WebGL 默认走 Metal 后端,同内容 GPU 负载显著更高("内容本质是 GPU 受限"),另有帧呈现依赖(254912,可致有效 30fps)等系统问题;Chrome/Android/macOS Safari 无此问题。
对策(已落地):
- MSAA 全平台开启(2026-08-06 产品决策:视觉统一如桌面端,不按平台预降档;iOS 上 MSAA 作用于整个帧缓冲、是最大开销之一,其成本由 governor 实测自适应降档兜底——iOS 复现卡顿优先疑这里)
- 静态背景烘焙到离屏纹理(resize 时重建),每帧一次不透明 blit;动态层保持混合
- 不透明/混合两趟绘制:PowerVR 平铺 GPU 上全屏混合直接放大成本
- blend 状态每帧幂等重设:canvas 尺寸变更会重置上下文状态,init 里设一次会失效
- 渲染门控加容差(
>= SIM_DT_MS - 1):60Hz 下 rAF 抖动会跳过半数渲染 → 16/33ms 交替伪 30fps
- 调试:
?dev=1 叠加层(app/dev/perf.ts)实时 fps/p95/max/tick/batch/load/顶点/上传/粒子档/dpr
信号:只有 iOS Safari 卡、其他平台都好 → 先怀疑 Metal 后端渲染路径,别动物理。
I7 headless Chrome 默认无 WebGL
症状:headless Chrome(--headless=new)里 getContext('webgl') 返回 null,游戏画面空白/只有 CSS 背景色;还容易误判为产品 bug。
修法:加 --enable-unsafe-swiftshader(软件 WebGL)。注意 SwiftShader 性能不代表真机,只用于管线正确性验证。
I9 AssemblyScript 整数字面量相除会截断(2026-08,#23 逐位对照抓出)
症状:AS 移植的几何代码与 JS 逐位对照时,插值系数(如 7/27)产出 0 而非 0.259——渐变环颜色全错,画面却"看起来正常",目测无法发现。
根因:AS 静态类型:7 / 27 两操作数推断为 i32,整数除法截断为 0;JS 同写法是浮点除。
修法:显式浮点 <f64>7 / 27(或 7.0 / 27.0)。移植 JS 数值代码后必须跑逐位对照(旧/新实现同场景比对 Float32Array),别靠目测。
信号:AS/wasm 移植后视觉"差不多但不对"、逐位对照出现整常数差异。
I10 数值内核迁移 Moonbit:位级一致方法论与 ±0 豁免(2026-08)
背景:数值内核自 AssemblyScript 迁移至 Moonbit(moon/ 模块,wasm 目标)。混沌流场(李雅普诺夫放大)下任何舍入漂移都会指数放大、改变通关可复现性,故迁移验证必须逐位。
方法:双引擎同输入对拍——同场景驱动新旧两实现,比对场字节(Uint32 视角);PRNG(mulberry32)用 UInt 回绕 + 逻辑右移对齐 JS imul+>>>。迁移完成、旧实现删除后,把基线固化为 tests/engine-golden.test.ts 的 golden hash(FNV-1a),永久守护位稳定性。
±0 豁免:AS 自身在零符号位上不一致(SIMD 路径 f64x2.min/max 走 wasm 语义 min(−0,+0)=−0,标量尾列比较链忽略零符号),"含零符号逐位一致"本就不是良定义不变量;对拍时零值只比数值不比符号,golden hash 钉死的是迁移后实现的输出。
附带:SIMD 已于 2026-08-16 回归(gs_pair/buoyancy2 v128 快路径,见 I8/G20),模块声明 simd 特性——I8 的 bun SIMD 误编译纪律重新适用。FixedArray 经内联 WAT 取数据区首地址交宿主零拷贝 view,该 ABI 非文档化,由 canary 握手测试(tests/engine-wasm.test.ts)守护。
信号:改 moon/ 数值代码后 engine-golden 失败——先确认是否有意改物理;有意则人工确认后更新基线,无意即回归。
附注(Math. 跨引擎末位分歧)**:Math.hypot 在 V8 与 JSC 末位实现不一致(实测 hypot(2.5, 7.1) 差 1 ulp)——golden 捕获运行时与 vitest 运行时不同即误报。sdf 内核距离一律 sqrt(a²+b²)(IEEE 精确运算,跨引擎逐位确定);min/max/abs 经 extern "js" 直通 Math.(语义含 NaN 传播需逐位同源,且这三者跨引擎一致)。
H3 Lit 3 样式在 shadowRoot.adoptedStyleSheets
无 <style> 标签,查生效规则读 cssRules 的 cssText。
H4 DevTools/CDP 附加使 wasm 降速约 10 倍——性能异常先排除调试器附加(2026-08 实锤)
症状:桌面 Chrome 玩时倍速切换看似无效(0.5/4/16 无可见差异)、16× 放置源有延迟;换环境(手机/关 DevTools)又正常。看似代码或部署问题,实为测量/观察环境问题。
根因:Chrome 被 DevTools 或 CDP 调试器附加(含 IDE 内置预览浏览器、MCP 自动化浏览器——它们恒附加)时 wasm 执行降速约 10 倍(实测同模块同 V8 系:node 1.35ms/tick、bun 0.94、无附加 headless Chrome 1.3–1.6;CDP 附加后主线程与 worker 皆 13.5–15ms,纯 JS 不受影响)。worker 吞吐被压到 ≈70 tick/s,1:1 回执背压(D20)丢弃超额 tick → 一切 >1.2× 的倍速封顶 ≈1.18(4×≈16×)、消息积压造成放置延迟。纯 JS 基准正常可排除进程限流。
修法:wasm 性能测量一律在无调试器附加的环境:headless Chrome 独立 user-data-dir(不开 --remote-debugging,结果经 localStorage + 第二页 dump-dom 取回)或 node/bun 无头基线;用户报告的性能异常先问“当时是否开着 DevTools/IDE 内置浏览器”。
信号:wasm 慢但纯 JS 快;同一构建在 headless 无附加环境正常;现象随 DevTools 开关出现/消失。
I11 站点分发的 .md 文档中文乱码(text/markdown 无 charset)
症状:Cloudflare 部署或 vite dev/preview 直接打开 /SKILL.md、/skills/level-design/.md,中文显示为 烧风技能指引 式乱码;文件字节本身是正确 UTF-8。
根因:sirv 与 Cloudflare 静态服务对 .md 均发 text/markdown 且不带 charset;浏览器对 text/ 无 charset 时默认按 Latin-1(windows-1252)解码,UTF-8 中文即乱码。JSON 默认 UTF-8 不受影响,只有 text/* 家族踩坑。
修法:dev/preview 由 vite.config.ts 的 md-utf8 插件在静态中间件前为 .md 补 Content-Type: text/markdown; charset=utf-8(sirv 尊重已设 Content-Type,不覆盖);生产由 public/_headers 对 /SKILL.md 与 /skills/* 声明同一头。
信号:新增任何随 public/ 分发的 .md/.txt 文档后,先在 dev server 直接打开确认中文正常;云上验证 curl -sI 看响应头 charset。
I12 MCP 布尔断言必须解析类型:'false' 字符串是 truthy
症状:chrome-devtools-mcp 自动化里 evalVal 断言"应为 true"实际恒真,脚本全绿但被测功能根本没发生(如 offline-verify 的 ?lv=1 深链检查从未验证过游戏屏)。
根因:MCP evaluate_script 返回文本,仿 .local/offline-verify.ts 的 evalVal 只把含 {...} 的文本解析成对象,裸 true/false 原样返回字符串——if (!gameReady) 对 'false' 判真,断言失效。
修法:断言前显式归一(gameReady === 'true' 或 JSON.parse 后再判);归一须覆盖脚本内所有布尔断言(offline-verify 的标题屏、在线/离线深链检查的布尔门全部失效,不只深链一处);离线/在线深链等关键路径用真实关卡 slug(如 ?lv=luo-yu)——?lv=1 不是合法关卡 id(level id 是 slug),解析失败落标题屏。
信号:MCP 脚本对布尔型 evaluate 结果做 !x 判断的地方逐一核对返回类型;关卡深链 URL 先确认 resolveLevel 能命中。
I13 wasm 共享内存注入 + COI/gtag 陷阱(2026-08 实测)
背景:moonc 产出的 wasm 是普通 Memory;SAB 跨线程零拷贝要求 Memory 带 shared 位。二进制注入(scripts/patch-shared.ts)只改一处:memory 段 limits flags 0x01(has_max)→0x03(has_max|shared)——threads 提案官方编码(shared 位 = limits flags 第 2 位;共享 Memory 要求 has_max,moonc 产物恒 min=max 满足)。已实证 node 实例化成功、memory.buffer instanceof SharedArrayBuffer === true、tracer golden 逐位一致。
不追加 target_features 段(2026-08 调研定案):该段是 tool-conventions 的链接器约定,不是运行时信号——三大引擎源码均不消费(V8 module-decoder/wasm-features/wasm-serialization、JSC WasmSectionParser、SpiderMonkey WasmValidate 全 0 命中,浏览器实测实例化正常)。规范编码 = count 前缀 +('+' + 特性名字符串,如 atomics/bulk-memory/simd128);曾误写 2b 01 02 04(无 count、数字 ID 当名字),binaryen 严格解析(读完须对齐段尾,否则 throwError)与 LLVM(先读 count)都会报错——坏段比无段更糟,故删。本产物是终产物不经链接,shared 位是运行时唯一信号。
症状 A:worker 内共享 Memory 实例化失败(RangeError: WebAssembly.Instance: ...)或主线程拿到非 SAB。
根因:COOP/COEP 缺失——浏览器只在跨域隔离上下文允许共享 Memory;COEP require-corp 还会阻塞所有无 CORP/ACAO 的跨源子资源。
修法:public/_headers 全站 Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp;vite dev/preview 必须同值(server.headers/preview.headers),否则本地调试与线上行为不一致。
症状 B(COI 后,可能):googletagmanager.com/gtag.js 被 COEP 拦(ERR_BLOCKED_BY_RESPONSE),GA 上报全断。
实测(2026-08):本仓库 COI 下 gtag.js 与 GA collect 均未被拦(200)——Google 静态分发响应带跨源许可头(CORP/CORS),COEP require-corp 放行;无需 credentialless 备选。若换用无跨源头的第三方资源被拦,备选 Cross-Origin-Embedder-Policy: credentialless(放行无 CORP 跨源资源但不带凭据;Safari 需 15.2+),或改服务端代理;第三方响应头不受控,CORP 方案不可行。
信号:改 COI 后在 devtools Network 看 gtag 请求状态;SAB 断言用 memory.buffer instanceof SharedArrayBuffer(node/bun 无 COI 限制,可直接验证注入生效)。
症状 C(非 COI 托管,2026-08 实测):部署到不能自定义响应头的静态托管(itch.io 等)→ crossOriginIsolated=false → worker 的 ready 消息携带 SAB postMessage 抛 DataCloneError: SharedArrayBuffer transfer requires self.crossOriginIsolated → 游戏画面冻结、仅 console 报错(wasm 实例化本身不抛,坑在于失败点不在实例化而在 SAB 传输)。
修法(已被下方兼容回退取代):main.ts 曾预检 crossOriginIsolated,缺失即明示屏(sf-unsupported reason=coi);worker 运行期崩溃经 controller onFatal → sf-game UNSUPPORTED(reason=fatal) 走明示屏的机制保留。
接受的设计取舍:主线程直读 SAB 与 worker 写入并发——单粒子可能读撕裂(单帧毛刺),视觉可忽略;零拷贝收益(worker −0.4ms/tick、主线程 −0.15ms/帧、堆减半,2026-08 实测)大于撕裂风险。
兼容回退(2026-08 落地):无 SAB 能力环境不再拦截而是降级:特性检测用 typeof SharedArrayBuffer(非 COI 浏览器中该全局不存在,比 crossOriginIsolated 更贴本质;WebView 架构上不支持 COI——Chromium issue 40914606,与响应头无关,头部方案无解)。engine.ts 引导期清 shared 位回退普通内存(段解析与 patch-shared.ts 单源在 wasm-shared.ts),worker 逐 tick 拷贝场/示踪随 frame transfer 送出(典型关卡 ~350KB/帧),渲染层零分叉;shared 位纯元数据,golden 基线不受模式影响(engine-wasm.test 钉死兼容实例化)。main.ts 不再预检 COI,明示屏只剩 WebAssembly 缺失。
:双模式落地后真机验证暴露边界问题(非安全上下文/内网 IP/老内核上双模式分流本身就是故障面),叠加收益复盘:SAB 快路径省 ~0.5ms/帧且只在 COI 环境生效——全是性能富余的高端机,真正需要性能的老设备/WebView 架构性无 COI 永远走拷贝路径(零覆盖优化)。遂整体移除:删 COOP/COEP 头(_headers + vite.config)、删构建期二进制注入(patch-shared.ts/wasm-shared.ts,产物回归 moonc 原样)、删双模式协议(ready.sab/frame.views 可选 → frame 恒带 views 拷贝)、bootEngine 还原纯检测。代价 = 每帧搬运(典型 ~350KB、满网格上限 ~1MB)+ GC 压力,governor 降级机制不变;未来若扩 tracer/网格致拷贝成本显著再议瘦身,不预建。收益 = 部署解除“能自定义响应头”约束(任意静态托管/HTTP/内网 IP 等价运行)、失败面减半、净删 ~200 行。教训:跨线程零拷贝类优化先量化目标设备覆盖率,零覆盖的收益不抵双模式复杂度;本条上文全部修法已成历史(git 可考古),保留作 COI/SAB 行为事实参考。
I14 GA snippet 延迟注入导致上报静默全断(2026-08 实测)
症状:Network 里 gtag/js 正常 200、_ga cookie 照写、事件确实进了 dataLayer,但零条 collect 请求;同页手动 fetch/beacon 到 collect 端点都通,甚至现场再注入一套全新标准 snippet 也不发——与业务代码无关的页面级静默。
实测排查(2026-08):把 snippet 从 index.html 内联改为 JS 延迟注入(wasm boot 后 injectSnippet)后复现;干净对照页(同库同命令)正常发。已排除:SW、CDP 附加、body 清空、注入延迟本身、网络。生产页 google_tag_data.ics.usedDefault 恒 false(consent 命令未被库应用),怀疑新版沙箱架构 gtag 的启动流水线在特定环境挂起;具体触发因子未钉死(HTTP 对照页正常、生产 HTTPS 复现,本地自签证书复现被证书状态污染不可作数)。
修法(已落地):snippet 恢复 index.html head 内联(consent default + js + config,与库加载时序解耦);analytics-gtag.ts 只留 transport = typeof gtag 守护 + 发事件(全局 gtag 由 @types/gtag.js 类型化),不再做任何注入。
信号:gtag/js 200 但无 collect → 查 window.google_tag_data?.ics?.usedDefault;恒 false 即库启动流水线异常,别在业务代码里找。
I15 tsconfig.node.json 覆盖 types 导致新 @types 半生效(2026-08 实测)
症状:新增 @types 包并加进 tsconfig.app.json 的 types 后,tsc -p tsconfig.app.json 通过,但 tsc -b(= bun run typecheck/check)仍报「Cannot find namespace/name」。
根因:tsconfig.node.json extends app 但覆盖了 compilerOptions.types(加 @types/bun 时整组替换),且 include 含 app/——同一文件在 node 项目下缺类型。-b 按 solution 编译两个项目,只改 app 是半生效。
修法:新 @types 两个 config 的 types 数组都加(本例 "gtag.js")。诊断信号 = 同一 tsc 版本下 -p 单项目过、-b 挂,必查 extends 链上 types 是否被覆盖。
I16 worker 构造抽变量致 Vite 不打包 worker(2026-08 实测)
症状:构建不报错,运行期 worker 加载指向未编译的 .ts 源码(或直接 404)。
根因:Vite 对 new Worker(new URL('…', import.meta.url), …) 是静态语法模式检测(含 options 可选参数形式);把 new URL(…) 抽成变量再传入即打断检测,worker 不进打包流水线,且 dev 下表面正常(dev 直接服务源码)——只有 build 产物会坏。
修法:new Worker( 与 new URL( 恒保持内联字面形态;若同处存在多处构造(如回落分支)每处都内联。验证 = build 后 dist/assets 存在 worker 独立 chunk 且主 chunk 引用其 hash 文件名。