模块二 · 工程化与网络
浏览器渲染与性能:卡顿到底发生在哪一步
卡顿不是"代码慢",而是主线程被长任务占住、布局被反复触发或合成层过多。这一节按流水线定位,并给出可验证的优化手段。
读完这一节,你能做到
- 说出渲染流水线的五个步骤。
- 判断一次修改会触发重排、重绘还是只合成。
- 用 Performance 面板定位长任务与强制同步布局。
- 解释 INP 与交互延迟的关系。
一、渲染流水线
Style(样式计算)→ Layout(布局)→ Paint(绘制)→ Composite(合成)
- Style:把 CSS 规则匹配到节点,算出最终样式。
- Layout:计算几何位置与尺寸,也叫重排。
- Paint:生成绘制指令,填充像素。
- Composite:把图层合成到屏幕,通常由 GPU 完成。
不是每次修改都走完整条链路:改 transform/opacity 通常只触发合成;改宽高会触发布局;改颜色只触发绘制。
二、重排、重绘与合成
| 修改 | 触发 | 成本 |
|---|---|---|
width、height、top、margin | Layout + Paint + Composite | 高 |
color、background、box-shadow | Paint + Composite | 中 |
transform、opacity、filter | Composite | 低 |
三个性能杀手:
- 强制同步布局:读
offsetHeight后又写样式,浏览器被迫立刻布局。 - 布局抖动:在循环里反复读写的,每次都触发一次布局。
- 过多合成层:
will-change滥用会占用显存,反而更卡。
修法:把读和写分批,先读所有布局信息,再统一写样式;will-change 只在动画期间加,结束后移除。
三、长任务与交互延迟
浏览器主线程同时处理 JS、样式、布局、绘制。一个 200ms 的长任务会阻塞输入,用户感觉“点了没反应”。
| 指标 | 含义 | 目标 |
|---|---|---|
| TBT | 长任务阻塞总时间 | 越低越好 |
| INP | 交互到下一帧绘制的延迟 | < 200ms |
| FPS | 每秒帧数 | 稳定 60 |
优化手段:
- 拆分长任务:把大循环切成小片,用
setTimeout或scheduler.postTask让出主线程。 - 移出主线程:纯计算放 Web Worker。
- 减少 DOM 数量:虚拟列表。
- 避免在滚动/输入回调里做重计算,必要时防抖节流。
防抖与节流:控制执行频率
长任务耗性能,频繁触发同样耗性能。搜索框每敲一个字就发请求、滚动每移动 1px 就算一次,都会把主线程塞满。
| 策略 | 行为 | 适用 |
|---|---|---|
| 防抖 debounce | 停止触发 N 毫秒后才执行一次 | 搜索输入、窗口 resize、表单校验 |
| 节流 throttle | 每 N 毫秒最多执行一次 | 滚动监听、拖拽、鼠标移动 |
防抖实现
function debounce(fn, wait = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
节流实现(时间戳版)
function throttle(fn, wait = 300) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
}
};
}
怎么选:关心「最终结果」用防抖(用户停手后再搜),关心「过程中的持续反馈」用节流(滚动进度条)。
两个进阶点
- 需要立即执行一次时,防抖加
immediate选项;需要保证最后一次不丢,节流用「时间戳 + 定时器」双实现。 - 与渲染相关的动画优先用
requestAnimationFrame,它跟着浏览器刷新节奏走,比定时器更稳。
别忘了清理:组件卸载时取消防抖的定时器,否则回调会在组件销毁后执行。
四、内存与泄漏
常见泄漏:
- 全局变量持有 DOM。
- 未移除的事件监听。
- 未清理的定时器与订阅。
- 闭包长期引用大对象。
- 脱离文档的节点仍被 JS 引用。
排查:Memory 面板做两次快照,对比对象增长;Performance 录制一段操作,看内存曲线是否持续上升。
五、工具与测量
| 工具 | 用途 |
|---|---|
| Performance | 火焰图、长任务、布局次数 |
| Lighthouse | 首屏与最佳实践评分 |
| Memory | 堆快照、泄漏对比 |
| Rendering | 高亮重绘、布局偏移、FPS |
| Coverage | 未使用 CSS/JS 比例 |
测量原则:固定设备与网络条件,先记录基线,再只改一个变量,最后对比同一指标。
六、常见坑
- 用
setInterval做动画,帧率不稳定。 - 在
scroll回调里读布局,造成强制同步布局。 - 给所有元素加
will-change,显存爆掉。 - 只看 FPS,不看 INP。
- 优化了 JS,却没发现瓶颈在图片解码或字体。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 用 Performance 录制一次滚动,找出最长的任务与布局次数。
- 写一个强制同步布局的例子,再用“读写分离”修掉。
- 用 Rendering 面板高亮重绘,观察改颜色与改 transform 的差异。
② 让 AI 写同一题
把第 2 题(卡顿定位)交给 AI,补一句 先给定位步骤,再给优化手段。
③ 对比要盯的点
- 重排与重绘它区分清楚了吗?“改变宽高”与“只改颜色”不是一类。
transform/opacity为什么能走合成层,它说对了吗?- 长任务(long task)它提了吗?卡顿的主因往往是主线程被占满。
④ 一句话取舍
它的优化手段里,哪一条在你这个页面上根本用不到?
八、自测
- 渲染流水线的五个步骤是什么?
- 哪些属性只触发合成?
- 什么是强制同步布局?
- INP 与 FPS 的区别是什么?
will-change为什么不能滥用?- 防抖和节流分别适合什么场景?各自的实现要点是什么?
参考答案要点:Style/Layout/Paint/Composite;transform/opacity;读写交替;交互延迟 vs 帧率;显存与层爆炸;关心最终结果用防抖、关心持续反馈用节流,注意清理定时器。
附录:高频追问
问:transform 为什么不触发布局? 它在合成阶段处理,不改变文档流中的几何信息。
问:position: fixed 一定开新层吗? 不一定,取决于浏览器与合成策略;不要依赖它做性能优化。
问:为什么读写分离能减少布局? 浏览器会批量处理写操作,集中读写避免每次读都触发一次布局。
问:requestAnimationFrame 能解决卡顿吗? 只能让回调与帧对齐,不能减少任务本身的开销。
问:Web Worker 能操作 DOM 吗? 不能,它没有 DOM;只能计算并把结果通过消息传回主线程。
问:虚拟列表为什么快? 减少 DOM 数量与布局计算,只渲染可视区域。
问:图片会导致卡顿吗? 会,解码在主线程(部分格式在合成线程),大图要压缩、懒加载、预留尺寸。
问:怎么判断是 JS 还是渲染的问题? Performance 火焰图里看长任务是在 Scripting 还是 Layout/Paint。
合成层与 GPU
合成层由 GPU 处理,适合动画与滚动。但层不是越多越好:每层都占用显存,层爆炸会导致上传纹理变慢,反而掉帧。
原则:只给“正在动的元素”加 transform/will-change,动画结束后移除;不要为了“看起来优化”给整页加层。
滚动性能的三个坑
- 在
scroll回调里读offsetTop,每帧强制布局。用IntersectionObserver或缓存位置。 - 给滚动容器加
background-attachment: fixed,某些浏览器会每帧重绘。 - 大量阴影与模糊,合成成本高。用简单的替代方案或减少层数。
一次掉帧的定位过程
现象:列表滚动时明显卡顿。
- 用 Performance 录制滚动,看 FPS 与长任务。
- 发现每帧都有 Layout,且耗时 8-12ms。
- 定位到
scroll回调里读取offsetTop,触发强制同步布局。 - 改成缓存位置 +
IntersectionObserver,Layout 消失。 - 同时把列表改成虚拟滚动,DOM 从 2,000 降到 20。
结果:FPS 从 25 到 58。复盘:先定位到“布局”而不是“JS 慢”,优化才不会白做。
一帧的预算
60fps 意味着每帧只有约 16.7ms。这段时间要分给:事件处理、JS、样式、布局、绘制、合成。任何一段超支,就会掉帧。
因此优化的方向不是“让 JS 更快”这一件事,而是“把每帧的工作量降下来”:减少 DOM、减少布局、把计算移出主线程。
工具的使用顺序
先用 Network 判断资源,再用 Performance 判断主线程,最后用 Memory 判断泄漏。顺序反了,容易在错误的方向上优化。工具只是证据,不是结论。
性能预算
首屏 JS < 200KB(gzip)、LCP < 2.5s、INP < 200ms、CLS < 0.1。预算写进 CI,才有人守;只写在文档里,发布时一定会破。一帧做好一件事,先降量,再提速;预算是纪律,测量是证据。慢就慢在布局,别猜,先量。先量再改,别猜。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★☆ 重排和重绘有什么区别?哪些操作会触发它们?
- ★★★★☆ 浏览器的本地存储方式有哪些?怎么选?
- ★★★☆☆ 浏览器多进程架构是怎样的?渲染进程负责什么?
- ★★★★☆ 前端错误监控怎么定位到源码?Source Map 应该怎么管理?
提示:读完可以顺手做几道对应题目,检验是否真的理解