浏览器渲染原理

1. 核心渲染流程

浏览器从接收到 HTML 数据到在屏幕上画出像素,主要经过以下步骤:

  1. 构建 DOM 树 (DOM Construction):
    • 解析 HTML 标记,构建 DOM (Document Object Model) 树。
    • HTML -> Tokens -> Nodes -> DOM Tree。
  2. 构建 CSSOM 树 (CSSOM Construction):
    • 解析 CSS (外部样式表、内联样式),构建 CSSOM (CSS Object Model) 树。
    • 注意:CSSOM 构建会阻塞渲染(Render Blocking),因为没有样式无法确定页面长什么样。
  3. 构建渲染树 (Render Tree Construction):
    • 将 DOM 树和 CSSOM 树合并。
    • 关键点display: none 的节点不会出现在渲染树中,但 visibility: hidden 的节点会(因为它占据空间)。
  4. 布局 (Layout / Reflow):
    • 计算渲染树中每个节点在屏幕上的确切位置和大小(几何信息)。
    • 输出:盒子模型 (Box Model)。
  5. 绘制 (Paint):
    • 将布局后的节点转换为屏幕上的像素(填充颜色、文字、边框、阴影等)。
    • 这是一个耗时过程,通常会分层 (Layers) 绘制。
  6. 合成 (Composite):
    • 将多个图层 (Layers) 按照正确顺序合并成一个最终画面,显示在屏幕上。
    • 这一步由 GPU 处理(如果使用了硬件加速)。

2. 回流 (Reflow) 与 重绘 (Repaint)

这是面试中最常考的性能优化点。

(1) 回流 (Reflow / Layout)

当 DOM 的几何属性(位置、大小)发生变化,或者 DOM 结构发生变化时,浏览器需要重新计算元素的几何信息。

触发条件

  • 添加或删除可见的 DOM 元素。
  • 元素尺寸改变(margin, padding, border, width, height)。
  • 内容改变(文本变化、图片替换等)。
  • 浏览器窗口尺寸改变(resize)。
  • 获取某些属性offsetWidth, offsetHeight, scrollTop, clientTop 等(因为浏览器需要强制刷新队列来计算准确值)。

(2) 重绘 (Repaint)

当元素的外观属性(颜色、背景、阴影)发生变化,但不影响布局时,浏览器只需要重新绘制该元素。

触发条件

  • color, background-color, visibility, box-shadow 等。

(3) 区别与性能

  • 回流必将引起重绘,也就是 Reflow > Repaint
  • 回流的代价远大于重绘。

3. 绘制 (Paint) 与 合成 (Composite)

这两个过程紧接在布局 (Layout) 之后,是渲染管道最耗时的部分。理解它们对于深度的前端性能优化至关重要。

(1) 绘制 (Paint Phase)

浏览器有了 Render Tree(几何信息),但它还不知道每个层具体怎么画。Paint 阶段会生成一系列的 Paint Records (绘制指令)

  • 操作fill color, stroke text, border 等。
  • 特点:这一步生成指令比较快,但后续的 光栅化 (Rasterization)(将指令转化为实际的像素点)比较慢。

(2) 合成 (Composite Phase) 与 硬件加速

这是一个将所有图层 (Layer) 按照正确层叠顺序在 GPU 中合并的步骤。 关键:浏览器会把页面分为多个 层 (Layer)。每个层单独进行 Paint 和 Rasterization。

  • 在滚动或动画通过 transform / opacity 进行变换时,只会影响合成阶段
  • 因为每个层已经在之前缓存好了位图 (Bitmap),所以修改这些属性不会触发回流和重绘,只需在 GPU 层面重新对位图进行平移、旋转或透明度改变,性能极高。

(3) 层提升 (Layer Promotion)

某些 CSS 属性会强制触发硬件加速,暗示浏览器为元素创建一个新的层 (Compositing Layer):

  • transform: translateZ(0)translate3d (3D 变换)
  • will-change: transform / opacity (现代浏览器推荐的显式提示)
  • <video>, <canvas>, <iframe> 元素
  • filter (CSS 滤镜)
  • position: fixed (在某些高 DPI 屏幕下)

⚠️ 滥用层的代价: 虽然分层好,但每个层都需要独立的显存 (VRAM) 来存储纹理数据,这叫做内存膨胀 (Memory Bloat)。如果在几十个列表项上手动加了 will-change,整个页面的内存会直接爆炸,在移动端容易引发频发的浏览器 Crash。

4. 优化策略 (面试必答)

  1. 避免频繁操作 DOM:使用 DocumentFragment 或一次性修改 style.cssText
  2. 读写分离:避免在循环中交替读写 DOM 属性(会导致强制同步布局)。
    // ❌ 强制同步布局
    for (let i = 0; i < len; i++) {
        el.style.width = el.offsetWidth + 1 + 'px';
    }
    // ✅ 读写分离
    const width = el.offsetWidth;
    el.style.width = width + 1 + 'px';
  3. 使用 CSS3 动画:优先使用 transformopacity 做动画。
  4. 脱离文档流:对复杂的动画元素使用 position: absolute/fixed,使其脱离文档流,减少对其他元素的影响。
  5. 层提升 (Layer Promotion):对频繁变化的元素使用 will-change: transform 提升为合成层。

5. 常见面试题

Q: 为什么 CSS 放在头部,JS 放在底部?

  • CSS:CSSOM 是构建渲染树的必要条件。如果 CSS 放在底部,页面可能先渲染出无样式内容 (FOUC),这也是不好的体验。而且 CSS 不会阻塞 DOM 解析,但会阻塞渲染。
  • JS:JS 可能会操作 DOM 和 CSSOM。如果放在头部,下载和执行 JS 会完全阻塞 DOM 解析。除非使用 asyncdefer

Q:display: nonevisibility: hidden 的区别?

  • display: none: 节点不在渲染树中,触发回流。
  • visibility: hidden: 节点在渲染树中(占位),只触发重绘。