深入理解浏览器渲染流程

0 阅读6分钟

深入理解浏览器渲染流程

0. 事件循环复习

我们之前总结过:事件循环是主线程的工作方式,每执行完一个宏任务,就清空所有微任务,然后可能渲染页面,再取下一个宏任务。

重点来了:渲染到底是怎么发生的? 这就是本篇文章要讲的内容。


1. 为什么需要了解渲染流程?

你每天都在写 HTML、CSS、JS,但浏览器到底是怎么把它们变成屏幕上像素的?

搞懂渲染流程,你就能明白:

  • 为什么改 left/top 会卡,改 transform 却很丝滑
  • 为什么有些 CSS 属性改了开销大,有些开销小
  • 面试官问“重排重绘”时该怎么答

这是前端性能优化的基础,也是面试必考题。


2. 渲染流程五步走

浏览器拿到 HTML 和 CSS 后,会按顺序做这 5 件事:

步骤名称做了什么
1构建 DOM 树把 HTML 标签转成树形结构
2构建 CSSOM 树把 CSS 规则转成树形结构
3构建渲染树合并 DOM 和 CSSOM,过滤掉不可见元素
4布局(Layout)计算每个元素的位置和大小
5绘制(Paint)把像素画到屏幕上

第 4 步也叫 重排(Reflow),第 5 步也叫 重绘(Repaint)

如下图:

2.1 构建 DOM 树

浏览器从上到下解析 HTML,把标签转成树形结构的 DOM 对象。
例如:

<html>
  <body>
    <div>hello</div>
  </body>
</html>

会变成类似这样的结构(伪代码):

document
  └ html
      └ body
          └ div → text "hello"

注意<script> 标签会阻塞解析,因为 JS 可能修改 DOM。可以加 deferasync 避免阻塞。

2.2 构建 CSSOM 树

浏览器解析 CSS 文件或 <style> 标签内的样式,构建成 CSSOM 树(CSS 对象模型)。
CSSOM 记录了选择器与样式规则的对应关系,以及继承关系(比如 bodyfont-size 会传给子元素)。

CSS 不会阻塞 DOM 树的构建,但会阻塞渲染(因为需要完整的样式才能绘制)。

2.3 构建渲染树(重点)

渲染树 = DOM 树 + CSSOM 树,但会过滤掉不需要显示的东西。

具体操作:

  1. 只保留能看见的元素
    • display: none 的元素不进入渲染树(连占位都没有)
    • <head> 标签里的元素不进入渲染树
    • visibility: hidden 的元素进入渲染树(它占位置,只是看不见)
    • opacity: 0 的元素也会进入渲染树(透明也是可见的一种)
  2. 给每个节点附上计算好的样式
    从 CSSOM 里找到匹配的规则,经过层叠、继承、优先级计算,得到每个节点的最终样式。

示例:

<div style="display: none;">看不见我</div>
<div>看得见我</div>

渲染树里只有第二个 div,第一个直接被丢掉了。

为什么需要渲染树?
因为 DOM 树里有很多不参与页面绘制的节点(headscriptdisplay: none 的元素),直接拿着 DOM 树去布局会浪费性能。渲染树就是“最终要画到屏幕上的东西”的清单。

2.4 布局(Layout / 重排)

遍历渲染树,计算每个元素在屏幕上的精确位置和尺寸(宽、高、x、y)。
比如一个 div 宽度是父容器的 50%,就要算出实际像素值。

触发布局的情况

  • 首次渲染
  • 窗口 resize
  • 修改元素的几何属性**(宽/高/边距/位置)**
  • 添加/删除 DOM
  • 读取某些属性(offsetHeightgetComputedStyle 等)

布局是开销最大的步骤。

2.5 绘制(Paint / 重绘)

把每个元素画成像素:背景、边框、文字、阴影、图片等。
浏览器会把页面分成多个图层,分别绘制,最后合成。

触发绘制的情况

  • 改变背景色、文字颜色、边框颜色等(不影响位置)

3. 重排 vs 重绘(核心重点)

这两个概念必须分清。

对比项重排(Reflow)重绘(Repaint)
什么时候发生改宽高、边距、位置、增删 DOM、改字体等改颜色、背景、阴影、可见性等
开销很大(重新计算位置)中等(只重新涂色)
会触发另一个吗会,重排一定导致重绘不会,重绘不一定导致重排
优化建议尽量避免,或用 transform 替代可接受,但不要频繁

3.1 代码示例

//  坏:触发重排
box.style.width = '200px'
box.style.height = '200px'
box.style.margin = '10px'

//  好:合并修改,只触发一次重排
box.style.cssText = 'width:200px; height:200px; margin:10px;'

//  更好:用 transform 做动画,完全不触发重排/重绘
box.style.transform = 'translateX(100px)'

4. 哪些操作会触发重排?

  • width / height / margin / padding / border
  • font-size(文字大小影响盒子大小)
  • display(比如 noneblock
  • 添加或删除 DOM 元素
  • 改变窗口大小
  • 读取某些属性:offsetHeightoffsetTopscrollTopgetComputedStyle 等(浏览器被迫立即重排)

最后一条只是读一下,浏览器也得乖乖重排才能给你准确值。所以不要在循环里读这些属性。


5. 如何减少重排?

优化手段说明
合并样式修改cssText 或切换 class,不要一条一条改
让元素脱离文档流position: absolutefixed,它的重排不影响别人
批量插入 DOMdocumentFragment 先组装好,再一次性插入
动画用 transformtransform 走合成线程,不触发重排/重绘
避免读触发布局的属性不要频繁读 offsetHeight 等,如果必须读,先读好存起来

5.1 批量插入 DOM 示例

//  坏:每次插入都触发重排
for (let i = 0; i < 100; i++) {
  document.body.appendChild(div)
}

//  好:用 fragment 一次性插入
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
  fragment.appendChild(div)
}
document.body.appendChild(fragment)  // 只触发一次重排

6. transform 为什么快?

transform 不走布局和绘制,它直接进入合成阶段,由 GPU 处理。

简单理解:

  • left/top:改位置 → 触发重排 → 重绘 → 合成(主线程干,慢)
  • transform:跳过前两步 → 直接合成(合成线程干,快)

所以做动画时,能用 transform 就别用 left/top

/*  慢 */
.box {
  transition: left 0.3s;
  left: 0;
}
.box.active {
  left: 100px;
}

/*  快 */
.box {
  transition: transform 0.3s;
  transform: translateX(0);
}
.box.active {
  transform: translateX(100px);
}

7. 常见面试题

7.1 重排和重绘的区别?哪个更耗性能?

重排是重新计算位置和大小,开销大;重绘是重新涂色,开销中等。重排一定触发重绘,反之不一定。

7.2 哪些属性会触发重排?

widthheightmarginpaddingborderfont-sizedisplayposition 等。还有添加/删除 DOM、改窗口大小。

7.3 如何避免重排?

  • 合并样式修改
  • 使用 transform 做动画
  • 批量操作 DOM
  • 让元素脱离文档流

7.4 transformleft/top 有什么区别?

left/top 触发布局(重排),慢;transform 只触发合成,由 GPU 处理,快。

7.5 为什么有时候读 offsetHeight 会让页面变慢?

因为浏览器需要立即计算最新的布局才能返回准确值,这会强制重排。如果在循环里读,会反复触发重排,性能极差。


8. 总结一句话

浏览器渲染分五步:DOM 树 → CSSOM 树 → 渲染树 → 布局(重排)→ 绘制(重绘)。
重排慢,重绘快,动画用 transform最流畅。
优化核心:减少重排,合并操作,能用合成就合成。