浏览器渲染流水线:解析JavaScript、Style、Layout、Paint、Composite五个关键阶段

做前端动画优化,绕不开一个核心问题:浏览器到底是怎么把代码变成像素的?

我刚开始接触性能优化时,也踩过不少坑。那时候总觉得动画卡顿就是「代码写得慢」,后来才发现——很多时候,问题出在你触发了哪些渲染阶段上。

今天咱们就把浏览器渲染流水线拆开来看。一共五个阶段:JavaScript → Style → Layout → Paint → Composite。每个阶段都有它的脾气,摸透了,你就能精准地避开性能雷区。

核心认知:动画流畅度的本质,就是让浏览器在 16.67ms(60fps)内,尽可能少地走完这些阶段。走得越少,帧率越稳。

浏览器渲染流水线五阶段流程图 浏览器渲染流水线:五个关键阶段 JavaScript 解析/执行/修改DOM Style 样式计算/规则匹配 Layout 布局/几何计算 Paint 绘制/像素填充 Composite 合成/图层合并 只触发 Composite transform / opacity 触发 Layout + Paint + Composite 修改 width / height / left / top 触发 Paint + Composite 修改 color / background / box-shadow 💡 动画优化目标:尽量只触发 Composite 阶段 ← 越靠左的阶段,性能开销越大 →

1. JavaScript:一切从这里开始

这个阶段,浏览器会执行你的 JS 代码。可能是事件回调、动画帧请求、或者 DOM 操作。

说白了,你写的 element.style.width = '200px',或者用 requestAnimationFrame 驱动的动画逻辑,都在这里跑。

我的习惯:在动画循环里,尽量只做「数据计算」和「属性赋值」,别在里面做 DOM 查询。比如 offsetHeight 这种强制同步布局的操作,能省就省。

2. Style:样式计算

JS 改完样式属性后,浏览器开始算「最终样式」。它会遍历所有匹配的 CSS 规则,算出每个元素到底长什么样。

举个例子:你给一个 .box 设置了 width: 50%,浏览器得先找到父容器的宽度,再算出 50% 是多少像素。

这里有个坑——选择器越复杂,匹配越慢。我记得有一次排查卡顿,发现团队里有人写了 div > ul > li:nth-child(3) .item 这种选择器,每次样式计算都要回溯好几层。后来改成简单的 class 选择器,帧率直接上来了。

我曾经踩过的坑:在动画循环里连续修改多个元素的 class 名,导致 Style 阶段反复触发。后来改用 classList.toggle 批量操作,配合 will-change 提前告知浏览器,效果好了很多。

3. Layout:布局计算

这是最「重」的阶段之一。浏览器要计算每个元素的位置和尺寸——宽、高、x、y、边距、边框……

你想想看,一个页面几百个元素,改一个元素的宽度,可能引发「多米诺骨牌效应」:父容器撑大了,兄弟元素被挤下去,子元素重新换行……

哪些属性会触发 Layout?

  • width / height
  • padding / margin / border
  • top / left / right / bottom
  • display / float / position
  • font-size / line-height

我个人习惯:动画中尽量避开这些属性。能用 transform 代替 left/top,就用 transform。

4. Paint:绘制

Layout 算完了位置,Paint 开始「填色」。浏览器会把每个元素拆成一个个绘制指令——画矩形、画圆角、画阴影、画文字……

这个阶段的开销,取决于你要画的东西有多复杂。比如一个纯色背景,几微秒就画完了。但一个带 box-shadow、border-radius、gradient 的元素,绘制时间可能翻几十倍。

绘制内容 性能开销 说明
纯色背景 / 简单边框 低 几乎不影响帧率
圆角 + 阴影 中 注意控制阴影模糊半径
渐变 + 滤镜 高 尽量避免在动画元素上使用
大量文字 + 字体渲染 高 字体加载和字形渲染都很耗时

一个小技巧:用 Chrome DevTools 的「Paint flashing」功能,能直观看到哪些区域在反复重绘。如果发现大片区域闪烁,说明 Paint 开销过大,该考虑提升为合成层了。

5. Composite:合成

终于到最后一步了。浏览器把各个绘制层(比如一个浮动的弹窗、一个滚动列表、一个 fixed 导航栏)合并成一张最终画面。

Composite 本身很快——它只做「图层叠加」,不涉及重新计算位置或重新绘制。所以,只触发 Composite 的动画是最理想的。

哪些属性只触发 Composite?

  • transform(translate / scale / rotate / skew)
  • opacity

嗯,就这两个。你想想看,为什么 CSS 动画里推荐用 transform: translateX() 而不是 left?因为前者只走 Composite,后者要触发 Layout → Paint → Composite,整整多走两个阶段。

核心结论:动画性能优化的黄金法则——
✅ 优先使用 transform 和 opacity
❌ 避免在动画中修改 width、height、left、top、margin、padding 等触发 Layout 的属性

阶段对比速查表

阶段 触发条件示例 性能开销 优化方向
JavaScript DOM 操作、事件处理、动画帧 中 减少 DOM 查询,避免强制同步布局
Style 修改 class、内联样式 低-中 简化选择器,批量修改样式
Layout 修改宽高、位置、边距 高 用 transform 替代位置属性
Paint 修改颜色、背景、阴影 中-高 减少复杂绘制效果,提升为合成层
Composite 修改 transform、opacity 低 尽量只触发此阶段

最后说一句:理解这五个阶段,不是为了背概念,而是为了在写代码时能「预判」性能。每次你写一行样式,心里可以默念一下——这行代码会触发哪几个阶段?如果答案是 Layout + Paint + Composite,那就要警惕了。