前端

浏览器是如何渲染页面的?

从 HTML 解析到 GPU 上屏,详解渲染主线程与合成线程分工,以及 DOM 树、布局树、图层与位图如何串联成流水线。

#browser #rendering #interview

浏览器是如何渲染页面的?

当网络进程把 HTML 文档下载完成后,渲染进程会收到字节流,并往 渲染主线程 的消息队列里塞入一个 渲染任务。主线程在 事件循环 里取出该任务后,才开始「把文档变成屏幕上的像素」。

HTML 下载完成后渲染任务进入消息队列

这不是一次性的函数调用,而是一条 流水线:每个阶段有明确的输入和输出,上一阶段的产出是下一阶段的输入。可以把它想象成工厂里的装配线——材料从一端进去,成品从另一端出来。

渲染流水线总览:主线程五段 + 合成三段

总览:八个阶段

阶段执行者输入输出
HTML 解析渲染主线程HTML 字节流DOM 树 + CSSOM 树
样式计算渲染主线程DOM + CSSOM带 Computed Style 的节点
布局 Layout渲染主线程样式树布局树(几何信息)
分层 Layer渲染主线程布局树图层树
绘制 Paint渲染主线程图层绘制指令集(display list)
分块 Tiling合成线程绘制指令图层小块
光栅化 RasterGPU 进程块信息位图(bitmap)
画 Draw合成线程 + GPU位图屏幕像素

记忆口诀:

  • 主线程:Parse → Style → Layout → Layer → Paint
  • 合成线程 + GPU:Tiling → Raster → Draw

主线程极其繁忙——事件循环 里还要执行 JS、处理事件、跑计时器。渲染只是它众多职责之一;一旦某段 JS 长时间占用主线程,整条渲染流水线都会暂停(见 reflow、repaint 与 transform)。

第一步:HTML 解析

解析 HTML 时,遇到 CSS 就解析 CSS,遇到 JS 就执行 JS

为提高效率,浏览器在正式解析前还会启动 预解析(preload scanner),提前下载 HTML 里 <link><script> 引用的外部资源,减少主线程空等。

解析 HTML 得到 DOM 树与 CSSOM 树

解析完成后得到两棵树:

  • DOM 树:文档结构,对应 HTML 标签的嵌套关系
  • CSSOM 树:汇总 用户代理默认样式<style>、外链 CSS、行内 style 等全部 CSS 规则

CSS 为什么不阻塞 HTML 解析?

主线程扫到 <link rel="stylesheet"> 时,即使 CSS 文件还没下载完,也不会停下来等,会继续解析后面的 HTML。CSS 的下载与解析由预解析等机制并行处理,因此 CSS 默认不阻塞 DOM 树的构建

JS 为什么会阻塞 HTML 解析?

主线程扫到 同步 <script> 时,会 暂停 HTML 解析,先等 JS 下载并执行完。因为 JS 可能通过 document.write 等方式修改 DOM,必须保证 DOM 生成顺序与脚本执行顺序一致。

deferasynctype="module" 等会改变具体行为,但 「同步脚本阻塞解析」 是默认规则,也是 <script></body> 前、或把脚本移到底部的原因。

link 不阻塞解析,同步 script 会阻塞

同步脚本阻塞 DOM 构建

html
<p id="before">before</p>
<script>
  document.querySelector('#before').textContent = 'changed'
</script>
<p id="after">after</p>
<!-- 解析器在遇到 script 时必须先执行完再继续 -->

第二步:样式计算

主线程遍历 DOM,结合 CSSOM,为每个节点算出最终用于渲染的样式,即 Computed Style(计算样式)。

此阶段会把许多值「算实」:

百分比宽高 等仍可能待定——它们依赖 包含块,要在 布局 阶段结合几何上下文才能定稿。

第三步:布局(Layout)

布局阶段遍历带样式的节点,计算 几何信息:宽高、位置(相对包含块)、行盒高度等,产出 布局树(Layout Tree)

布局树 ≠ DOM 树

两棵树 常常不是一一对应

情况DOM 树布局树
display: none有节点无几何信息,不进布局树
::before / ::after无对应节点有几何信息,会进布局树
匿名行盒 / 块盒无独立节点布局算法自动生成

DOM 树与布局树并不总是一一对应

布局完成后,每个可见元素在屏幕上「占哪块地方」就确定了。改宽高、边距、display 等会触发 reflow(回流),需要重新跑 Layout 及之后阶段——详见 reflow 与 repaint

第四步:分层(Layer)

主线程按策略把布局树 分成多个图层(Layer)

分层是性能优化:将来只改某一层的样式或内容时,可以 只更新该层 的后续步骤,而不必重算整页。

常见影响因素:

  • 滚动容器、滚动条
  • 堆叠上下文(z-index 等)
  • transformopacity
  • will-change 提示浏览器提前分层

分层策略因浏览器而异,属于实现细节;开发上只需知道:分层是为了增量更新更快

第五步:绘制(Paint)

主线程为 每一层 单独生成 绘制指令集(display list),描述这一层该怎么画——类似 Canvas 的 fillRectdrawText 等调用序列。

图层与绘制指令

渲染主线程的工作到这里为止。 之后把各层的绘制信息交给 合成线程(Compositor Thread)

colorbackground 等可见但不改布局的样式,可能只需 repaint(重算绘制及之后),不必完整 reflow。

第六~八步:分块、光栅化、上屏

合成线程接手后:

1. 分块(Tiling)

把每一层划成更小的 图块(tile),交给 线程池 并行处理,避免单次任务过大。

2. 光栅化(Raster)

把块信息交给 GPU 进程,由 GPU 以极高速度转成 位图(bitmap)——每个块是一小块像素矩阵。靠近 视口 的块会 优先 光栅化,所以你滚动时远处内容可能稍晚才清晰。

3. 画(Draw)

合成线程为每块位图生成 指引(quad):在屏幕上的位置、旋转、缩放等。transform 的变形主要发生在此阶段,不经过主线程的 Layout/Paint,因此动画往往更顺滑。

分块、光栅化、quad、GPU 上屏

最后 GPU 进程通过系统调用把结果交给显卡。渲染进程跑在沙盒里,不能直接碰硬件,崩溃也不会拖垮整个系统。

常见面试答法(精简版)

网络进程收到 HTML 后,产生渲染任务进入渲染主线程的消息队列;事件循环取出任务,开启流水线:解析 → 样式 → 布局 → 分层 → 绘制 → 分块 → 光栅化 → 画。主线程负责前五步,合成线程和 GPU 负责后三步。CSS 不阻塞 DOM 构建,同步 JS 会阻塞解析。改动样式可能触发 reflow/repaint,是流水线运行中的局部重跑。

小结

  • 渲染由 事件循环调度,是主线程与合成线程协作的流水线。
  • DOM + CSSOM → 样式 → 布局 → 图层 → 绘制指令 → 位图 → 屏幕
  • 同步 JS 阻塞解析CSS 默认不阻塞 DOM
  • Layout 定几何Paint 出指令Compositor + GPU 上屏transform 多在合成阶段完成。
  • CSS 属性计算包含块reflow/repaint 串联起来,就是前端性能优化的完整地图。