浏览器渲染与 V8 内存问题记录
记录渲染流水线、GC、内存泄漏排查中踩过的坑,每个问题都是实际遇到过并解决掉的。
问题一:图表页一秒只跑 8 帧
现象:10 个图表实例更新时页面卡成幻灯片,Performance 面板全是紫色 Layout 事件。
原因:Layout Thrashing——各实例在自己的回调里交替"读 offsetWidth、写 style",每次读都使写队列失效并强制 reflow,一帧内跑了 N 次完整布局。
解决:跨组件统一读写队列(FastDOM 模式),一帧内先清空读队列再清空写队列:
class FastDOMQueue {
private reads: Array<() => void> = [];
private writes: Array<() => void> = [];
measure(fn: () => void) { this.reads.push(fn); this.schedule(); }
mutate(fn: () => void) { this.writes.push(fn); this.schedule(); }
private schedule() {
requestAnimationFrame(() => {
this.reads.splice(0).forEach(fn => fn()); // 读阶段
this.writes.splice(0).forEach(fn => fn()); // 写阶段
});
}
}
能用 CSS 的不用 JS:滚动驱动动画(animation-timeline: scroll())在合成器线程跑,彻底绕开主线程。
问题二:rAF 里 setState,DOM 到底什么时候上屏
现象:想精确预测"这帧改的样式用户这帧能不能看到",时序总是对不上。
原因:一帧的完整顺序是:task → 清空全部微任务 → rAF 回调 → style/layout/paint → rIC(空闲才跑)。rAF 里产生的微任务在本帧 paint 前执行;rIC 可能被连续渲染饿死。
解决:高频 DOM 写放 rAF(paint 前合并),纯计算分片放 rIC 且必须给 timeout 兜底;要拿"上一帧 paint 后"的结果用 requestPostAnimationFrame 思路。
问题三:白屏 1.2 秒,JS 已经 async 了还卡
现象:script 全部加了 async/defer,FCP 依然 1.2s。
原因:async/defer 对 CSS 无效——head 里的同步 CSS 必须下载+解析完(CSSOM)才开始渲染;字体 FOIT 也在卡。
解决:内联关键 CSS + 非关键样式 media="print" 异步加载;字体 font-display: swap + preload;用 Coverage/Performance 面板归因而不是猜。注意 async 的 JS 执行时依然抢主线程,首屏 JS 预算照样要控。
问题四:Node 服务 P99 抖动,对应 Major GC 停顿
现象:RSS 平稳但每 30 秒一次 40ms 停顿,接口 P99 毛刺。
原因:老生代 Mark-Compact 的 stop-the-world;新生代对象晋升过快导致 Major GC 频繁。
解决:增量/并发标记已默认开,能动的还有——压堆大小减少单次标记范围、减少跨代引用(长命对象别引用短命对象)、大 Buffer 直接进 Large Object Space 的认识要建立。监控用 performance.measureUserAgentSpecificMemory() 替代已废弃的 performance.memory。
问题五:用 WeakRef 做"自动清理资源"是定时炸弹
现象:想用 FinalizationRegistry 在对象被回收时自动 reconnect 服务,生产上偶发"资源泄漏但清理永远没执行"。
原因:GC 时机不可预期——回调可能几秒后执行、可能永不执行,且回调在微任务里跑不能抛错。
解决:WeakRef 只用于诊断和兜底(统计"该死没死的对象数"暴露泄漏、大缓存 miss 重算即可的场景),业务清理必须靠显式生命周期管理。泄漏排查三步:
- Performance 录制看 JS Heap 阶梯上升不回落
- 堆快照 A/B 对比,按 Detached 过滤 DOM 节点
- Retainers 面板找强引用链——通常是未清理的监听器/定时器/闭包
高频泄漏代码模式:addEventListener 无 cleanup、setInterval 闭包拽住已移除的 DOM 节点(Detached)、全局缓存持有大数组。
