知

模块二 · 工程化与网络

第 8 / 14 节21 分钟阅读型

浏览器渲染与性能:卡顿到底发生在哪一步

卡顿不是"代码慢",而是主线程被长任务占住、布局被反复触发或合成层过多。这一节按流水线定位,并给出可验证的优化手段。

读完这一节,你能做到

  1. 说出渲染流水线的五个步骤。
  2. 判断一次修改会触发重排、重绘还是只合成。
  3. 用 Performance 面板定位长任务与强制同步布局。
  4. 解释 INP 与交互延迟的关系。

一、渲染流水线

Style(样式计算)→ Layout(布局)→ Paint(绘制)→ Composite(合成)
  • Style:把 CSS 规则匹配到节点,算出最终样式。
  • Layout:计算几何位置与尺寸,也叫重排。
  • Paint:生成绘制指令,填充像素。
  • Composite:把图层合成到屏幕,通常由 GPU 完成。

不是每次修改都走完整条链路:改 transform/opacity 通常只触发合成;改宽高会触发布局;改颜色只触发绘制。

二、重排、重绘与合成

修改触发成本
width、height、top、marginLayout + Paint + Composite高
color、background、box-shadowPaint + Composite中
transform、opacity、filterComposite低

三个性能杀手:

  1. 强制同步布局:读 offsetHeight 后又写样式,浏览器被迫立刻布局。
  2. 布局抖动:在循环里反复读写的,每次都触发一次布局。
  3. 过多合成层: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);
    }
  };
}

怎么选:关心「最终结果」用防抖(用户停手后再搜),关心「过程中的持续反馈」用节流(滚动进度条)。

两个进阶点

  1. 需要立即执行一次时,防抖加 immediate 选项;需要保证最后一次不丢,节流用「时间戳 + 定时器」双实现。
  2. 与渲染相关的动画优先用 requestAnimationFrame,它跟着浏览器刷新节奏走,比定时器更稳。

别忘了清理:组件卸载时取消防抖的定时器,否则回调会在组件销毁后执行。

四、内存与泄漏

常见泄漏:

  • 全局变量持有 DOM。
  • 未移除的事件监听。
  • 未清理的定时器与订阅。
  • 闭包长期引用大对象。
  • 脱离文档的节点仍被 JS 引用。

排查:Memory 面板做两次快照,对比对象增长;Performance 录制一段操作,看内存曲线是否持续上升。

五、工具与测量

工具用途
Performance火焰图、长任务、布局次数
Lighthouse首屏与最佳实践评分
Memory堆快照、泄漏对比
Rendering高亮重绘、布局偏移、FPS
Coverage未使用 CSS/JS 比例

测量原则:固定设备与网络条件,先记录基线,再只改一个变量,最后对比同一指标。

六、常见坑

  1. 用 setInterval 做动画,帧率不稳定。
  2. 在 scroll 回调里读布局,造成强制同步布局。
  3. 给所有元素加 will-change,显存爆掉。
  4. 只看 FPS,不看 INP。
  5. 优化了 JS,却没发现瓶颈在图片解码或字体。

七、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 用 Performance 录制一次滚动,找出最长的任务与布局次数。
  2. 写一个强制同步布局的例子,再用“读写分离”修掉。
  3. 用 Rendering 面板高亮重绘,观察改颜色与改 transform 的差异。

② 让 AI 写同一题

把第 2 题(卡顿定位)交给 AI,补一句 先给定位步骤,再给优化手段。

③ 对比要盯的点

  • 重排与重绘它区分清楚了吗?“改变宽高”与“只改颜色”不是一类。
  • transform / opacity 为什么能走合成层,它说对了吗?
  • 长任务(long task)它提了吗?卡顿的主因往往是主线程被占满。

④ 一句话取舍

它的优化手段里,哪一条在你这个页面上根本用不到?

八、自测

  1. 渲染流水线的五个步骤是什么?
  2. 哪些属性只触发合成?
  3. 什么是强制同步布局?
  4. INP 与 FPS 的区别是什么?
  5. will-change 为什么不能滥用?
  6. 防抖和节流分别适合什么场景?各自的实现要点是什么?

参考答案要点: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,动画结束后移除;不要为了“看起来优化”给整页加层。

滚动性能的三个坑

  1. 在 scroll 回调里读 offsetTop,每帧强制布局。用 IntersectionObserver 或缓存位置。
  2. 给滚动容器加 background-attachment: fixed,某些浏览器会每帧重绘。
  3. 大量阴影与模糊,合成成本高。用简单的替代方案或减少层数。

一次掉帧的定位过程

现象:列表滚动时明显卡顿。

  1. 用 Performance 录制滚动,看 FPS 与长任务。
  2. 发现每帧都有 Layout,且耗时 8-12ms。
  3. 定位到 scroll 回调里读取 offsetTop,触发强制同步布局。
  4. 改成缓存位置 + IntersectionObserver,Layout 消失。
  5. 同时把列表改成虚拟滚动,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,才有人守;只写在文档里,发布时一定会破。一帧做好一件事,先降量,再提速;预算是纪律,测量是证据。慢就慢在布局,别猜,先量。先量再改,别猜。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解