Skip to content

浏览器渲染流程 (Critical Rendering Path)

浏览器从获取 HTML 到最终在屏幕上显示图像,经历了一系列复杂的过程,通常称为关键渲染路径 (Critical Rendering Path)

1. 渲染流程概览

主要包含五个步骤:

  1. HTML 解析:构建 DOM 树。
  2. CSS 解析:构建 CSSOM 树。
  3. 构建渲染树 (Render Tree):结合 DOM 和 CSSOM。
  4. 布局 (Layout/Reflow):计算每个节点在屏幕上的位置和大小。
  5. 绘制 (Paint):将节点绘制到屏幕上。
    • (现代浏览器还有一步:合成 Composite)
mermaid
graph LR
HTML[HTML] --> DOM[DOM 树]
CSS[CSS] --> CSSOM[CSSOM 树]
DOM --> RenderTree[渲染树]
CSSOM --> RenderTree
RenderTree --> Layout[布局 Layout]
Layout --> Paint[绘制 Paint]
Paint --> Composite[合成 Composite]
Composite --> Display[显示]

2. 详细步骤解析

2.1 构建 DOM 树 (Constructing the DOM Tree)

  • 输入:HTML 字节流。
  • 过程
    1. 解码:将字节转换成字符。
    2. 令牌化 (Tokenization):将字符转换成 Token(如 <html>, <body>)。
    3. 生成节点 (Lexing):将 Token 转换成对象(Node)。
    4. 构建树:根据嵌套关系建立节点间的父子联系。
  • 输出:DOM 树(Document Object Model)。

2.2 构建 CSSOM 树 (Constructing the CSSOM Tree)

  • 输入:CSS 样式(外链、内联、样式表)。
  • 过程:类似于 DOM 构建,但 CSS 是级联的,浏览器需要计算出每个节点的最终样式(Computed Style)。
  • 输出:CSSOM 树(CSS Object Model)。
  • 注意:CSS 解析会阻塞渲染(Render Blocking),因为没有样式,渲染出的页面是乱的,浏览器宁愿白屏也不愿渲染无样式的页面。

2.3 构建渲染树 (Render Tree)

  • 过程:遍历 DOM 树的根节点。
    1. 可见性检查:忽略不可见的节点(如 <head>, <script>, display: none 的节点)。
      • 注意:visibility: hidden 的节点会包含在 Render Tree 中,因为它占据空间。
    2. 匹配样式:对于每个可见节点,找到在 CSSOM 中对应的规则并应用。
  • 输出:包含视觉信息的渲染树(每个节点也是一个渲染对象 RenderObject)。

2.4 布局 (Layout) / 回流 (Reflow)

  • 目的:计算渲染树中每个节点在视口(Viewport)内的确切位置和大小。
  • 过程:从根节点开始递归遍历,计算几何信息(位置、尺寸)。
  • 输出:每个节点的盒子模型(Box Model)信息。
  • 触发回流:当 DOM 变化影响了元素的几何属性(宽、高、位置)或结构时,浏览器需要重新计算布局。
    • 添加/删除 DOM 元素。
    • 改变窗口大小。
    • 修改字体大小。
    • 获取某些测量属性(如 offsetWidthscrollTop)会强制立即回流。

2.5 绘制 (Paint) / 重绘 (Repaint)

  • 目的:将渲染树中的每个节点转换成屏幕上的像素。
  • 过程:遍历渲染树,调用系统的图形 API 绘制背景、边框、文本、阴影等。
  • 触发重绘:当元素外观发生变化但没有改变布局(如 color, background, visibility)时触发。
  • 代价:重绘的代价比回流小,但仍然消耗性能。

2.6 合成 (Composite)

  • 现代浏览器的优化:为了提升性能,浏览器会将页面分层(Layer)。
  • 层 (Layer):拥有独立层级的元素(如 <video>, cancel, position: fixed, z-index, transform 3D)。
  • 过程
    1. 主线程计算好每层的绘制指令。
    2. 合成线程(Compositor Thread)接收指令,将层分成图块(Tile)。
    3. 栅格化线程(Raster Thread)将图块转换成位图。
    4. GPU 将位图绘制到屏幕。
  • 优势transformopacity 动画可以直接在合成线程处理,不触发 Layout 和 Paint,性能极高。

3. 回流 (Reflow) vs 重绘 (Repaint)

特性回流 (Reflow)重绘 (Repaint)
定义布局或者几何属性改变,需重新计算位置元素外观改变,不影响布局
触发条件宽高改变、增删 DOM、窗口大小改变颜色改变、背景改变、visibility
影响范围可能影响整个页面或部分树仅影响当前元素
性能开销极大较小
关系回流必将引起重绘重绘不一定引起回流

性能优化建议

  1. 批量修改 DOM

    • 使用 documentFragment
    • display: none 隐藏元素,修改完后再显示(只触发两次回流)。
    • 使用 cloneNode 在内存中修改后替换。
  2. CSS 优化

    • 避免使用复杂的选择器(选择器匹配是从右向左的)。
    • 避免设置多层内联样式。
    • 优先使用 transform 和 opacity 做动画(只触发合成,不回流不重绘)。
    • 将频繁变化的元素提升为独立层(will-change: transform)。
  3. JS 优化

    • 缓存布局信息(不要在循环中频繁读取 offsetWidth 等属性,会导致强制同步布局)。
    javascript
    // ❌ 强制同步布局 (Layout Thrashing)
    for (let i = 0; i < len; i++) {
        box.style.width = box.offsetWidth + 1 + 'px'; // 读 -> 写 (反复回流)
    }
    
    // ✅ 读写分离
    const width = box.offsetWidth;
    for (let i = 0; i < len; i++) {
        box.style.width = width + 1 + 'px';
    }

4. 为什么 CSS 放头部,JS 放尾部?

CSS 放头部 (<head>)

  • 避免闪烁 (FOUC):如果 CSS 放在底部,浏览器先渲染无样式的 HTML,加载完 CSS 后又重绘一次,用户会看到页面样式突然变化。放在头部可以让浏览器在渲染前就准备好样式。
  • CSS 不阻塞 DOM 解析:但会阻塞渲染,所以越早加载越好,以便 CSSOM 尽早构建完成。

JS 放尾部 (<body> 结束前)

  • 避免阻塞渲染:JS 执行会阻塞 GUI 渲染线程。如果 JS 放在头部且执行时间长,用户会看到白屏。
  • 确保 DOM 已加载:JS 通常用来操作 DOM,放在底部可以保证脚本执行时 DOM 树已经构建完成,避免找不到节点的错误。
  • 现代方案:使用 <script defer><script async> 放在头部。

5. defer 与 async 的区别

html
<script src="script.js"></script>
<script async src="script.js"></script>
<script defer src="script.js"></script>
模式解析 HTML加载 JS执行 JS执行时机
普通停止并行下载阻塞加载后立即执行遇到标签时
async继续并行下载阻塞下载后立即执行下载完立即执行(乱序)
defer继续并行下载等待HTML 解析完成后DOMContentLoaded 前(顺序)
  • Async:适用于独立的脚本(如统计分析),不依赖其他脚本,也不被依赖。
  • Defer:适用于应用逻辑脚本,保证执行顺序,且不阻塞解析。

MIT Licensed | Keep Learning.