浏览器渲染原理
1. 核心渲染流程
浏览器从接收到 HTML 数据到在屏幕上画出像素,主要经过以下步骤:
- 构建 DOM 树 (DOM Construction):
- 解析 HTML 标记,构建 DOM (Document Object Model) 树。
- HTML -> Tokens -> Nodes -> DOM Tree。
- 构建 CSSOM 树 (CSSOM Construction):
- 解析 CSS (外部样式表、内联样式),构建 CSSOM (CSS Object Model) 树。
- 注意:CSSOM 构建会阻塞渲染(Render Blocking),因为没有样式无法确定页面长什么样。
- 构建渲染树 (Render Tree Construction):
- 将 DOM 树和 CSSOM 树合并。
- 关键点:
display: none 的节点不会出现在渲染树中,但 visibility: hidden 的节点会(因为它占据空间)。
- 布局 (Layout / Reflow):
- 计算渲染树中每个节点在屏幕上的确切位置和大小(几何信息)。
- 输出:盒子模型 (Box Model)。
- 绘制 (Paint):
- 将布局后的节点转换为屏幕上的像素(填充颜色、文字、边框、阴影等)。
- 这是一个耗时过程,通常会分层 (Layers) 绘制。
- 合成 (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. 优化策略 (面试必答)
- 避免频繁操作 DOM:使用
DocumentFragment 或一次性修改 style.cssText。
- 读写分离:避免在循环中交替读写 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';
- 使用 CSS3 动画:优先使用
transform和 opacity 做动画。
- 脱离文档流:对复杂的动画元素使用
position: absolute/fixed,使其脱离文档流,减少对其他元素的影响。
- 层提升 (Layer Promotion):对频繁变化的元素使用
will-change: transform 提升为合成层。
5. 常见面试题
Q: 为什么 CSS 放在头部,JS 放在底部?
- CSS:CSSOM 是构建渲染树的必要条件。如果 CSS 放在底部,页面可能先渲染出无样式内容 (FOUC),这也是不好的体验。而且 CSS 不会阻塞 DOM 解析,但会阻塞渲染。
- JS:JS 可能会操作 DOM 和 CSSOM。如果放在头部,下载和执行 JS 会完全阻塞 DOM 解析。除非使用
async 或 defer。
Q:display: none 和visibility: hidden 的区别?
display: none: 节点不在渲染树中,触发回流。
visibility: hidden: 节点在渲染树中(占位),只触发重绘。