我不知道的浏览器:页面渲染流水线详解

361 字 14 min read
浏览器原理 渲染流程 性能优化 CSSOM DOM Reflow Repaint Compositing

1. 浏览器渲染页面的整体视角

当你在浏览器地址栏输入网址并按下回车后,浏览器就开始了一系列复杂的工作,最终目的是将服务器返回的 HTML、CSS、JavaScript 等资源,转换成我们能在屏幕上看到的色彩斑斓、可交互的网页。这个过程就像一条精心设计的流水线,涉及多个步骤和不同组件(如渲染进程的主线程、合成线程、GPU 进程等)的协同工作。理解这条流水线不仅能满足好奇心,更是前端性能优化的基础。


2. 渲染流水线的八个关键步骤

现代浏览器的渲染过程大致可以分为以下八个主要步骤,这些步骤主要由渲染进程的主线程和合成线程协作完成:

  1. 解析 HTML (Parse HTML)

    • 主线程接收到 HTML 数据后,开始解析,将字节流转化为字符,再将字符转化为令牌 (Tokens),最终构建成 DOM (文档对象模型) 树。DOM 树是 HTML 文档的结构化表示,也是后续操作的基础。
    • 预解析优化:在主线程解析 HTML 的同时,浏览器会启动一个预解析线程,扫描 HTML 中的外部资源链接(如 CSS、JS、图片),提前开始下载,以节省时间。
    • JS 阻塞:当解析器遇到 <script> 标签(非 async/defer)时,会暂停 HTML 解析,等待脚本下载并执行完毕。这是因为 JavaScript 可能会修改 DOM 结构,所以需要确保 JS 执行时 DOM 是稳定可用的。这就是"JS 阻塞 DOM 解析"的原因。
  2. 解析 CSS 与样式计算 (Style Calculation)

    • 主线程(或在预解析线程辅助下)解析 CSS 文件(外部、内部、内联样式),构建 CSSOM (CSS 对象模型) 树。CSSOM 包含了所有元素的样式信息。
    • CSS 不阻塞 DOM 解析,但阻塞渲染: CSS 的解析和下载通常不会阻塞 DOM 树的构建(因为可以在预解析线程进行),但会阻塞后续的渲染树构建和页面渲染。浏览器需要确保在渲染页面前,所有 CSS 规则都已加载并解析完成。
    • 计算样式: 主线程遍历 DOM 树,结合 CSSOM 以及浏览器默认样式、用户样式等,计算出每个 DOM 节点的最终应用样式 (Computed Style)。这个过程会解析各种单位(如 rem 转 px)、处理继承、层叠规则等。
  3. 布局 (Layout / Reflow)

    • 有了 DOM 树和每个节点的计算样式,主线程接下来需要计算每个节点在屏幕上的精确位置和大小。这个过程称为布局(Layout),有时也叫回流(Reflow)。
    • 输出是一个包含所有可见元素几何信息的布局树 (Layout Tree)(或称渲染树 Render Tree)。布局树与 DOM 树不完全对应,例如 display: none 的节点不会出现在布局树中,而伪元素(如 ::before)虽然不在 DOM 中,却会出现在布局树里。
  4. 分层 (Layer / Update Layer Tree)

    • 为了优化后续的绘制和合成,主线程会遍历布局树,识别出需要独立绘制和合成的区域,并将它们提升为单独的图层 (Layer)。这个过程生成了图层树 (Layer Tree)
    • 哪些情况会创建新图层?拥有特定 CSS 属性的元素,如 position: absolute/relative/fixed/sticky (需配合 z-index)、opacity < 1transform (非 none)、filterwill-change 明确指定的属性,以及 <video>, <canvas>, <iframe> 等元素,通常会触发浏览器创建新的合成层。
    • 过多的图层会消耗更多内存,并非越多越好。
  5. 绘制 (Paint / Record Paint)

    • 主线程为每个图层生成绘制指令。这些指令并非直接画出像素,而是记录了"如何绘制这个图层内容"的步骤列表(例如,"在坐标 (x, y) 处画一个背景色为红色的矩形")。
    • 主线程将这些绘制指令记录下来。
    • 耗时: 绘制所需时间取决于图层的复杂程度和应用的样式(如复杂的阴影、渐变等)。
  6. 分块 (Tiling)

    • 从这里开始,工作移交给合成线程 (Compositor Thread)
    • 由于图层可能很大,合成线程会将每个图层划分成多个图块 (Tiles),通常是 256x256 或 512x512 像素大小。这样做是为了后续的光栅化可以并行处理,并且优先处理视口(viewport)附近的图块。
  7. 光栅化 (Rasterization)

    • 合成线程将图块信息(包含绘制指令)发送给 GPU 进程
    • GPU 进程中的光栅化线程 (Raster Threads)(通常从线程池获取)负责将这些绘制指令实际转换成屏幕上的位图 (Bitmap) 像素信息。
    • GPU 利用其强大的并行处理能力高速完成光栅化,尤其是视口附近的图块会优先处理。
  8. 合成与显示 (Compositing and Display)

    • 光栅化完成后,合成线程收集到所有图块的位图信息。
    • 合成线程根据图层树的结构、视口信息、滚动位置、缩放、变换等,计算出每个图块最终在屏幕上的位置、变形等信息,生成合成帧 (Compositor Frame)
    • Transform/Opacity 的高效: 像 transformopacity 这样的属性,如果它们作用在独立的合成层上,合成线程可以直接在 GPU 上修改图块的位置或透明度来生成新的合成帧,无需重新经过主线程的布局和绘制阶段。这就是它们性能通常很高的原因。
    • 最后,合成线程将合成帧通过 GPU 提交给操作系统,最终显示在屏幕上。

3. CSSOM 的角色与可操作性

CSSOM 不仅是渲染的基础数据结构,开发者也可以通过 JavaScript 与之交互:

  • 访问样式表: document.styleSheets 可以获取页面加载的所有样式表。
  • 访问/修改规则: 可以遍历 cssRules 属性来查看或修改具体的 CSS 规则,例如 document.styleSheets[0].cssRules[0].style.color = 'blue';
  • 获取计算样式: window.getComputedStyle(element) 是非常有用的 API,可以获取元素经过所有样式规则计算后最终应用的样式值。

注意: 直接修改样式的操作(如 element.style.width = '100px')或频繁查询计算样式(尤其是在循环中),可能会触发强制同步布局(见下文),影响性能。


4. 合成线程与主线程的分工

总结一下渲染进程中两个关键线程的分工:

  • 主线程 (Main Thread): 负责 HTML/CSS/JS 解析、样式计算、布局、图层树更新、绘制指令生成。它是"决策者"和"规划者"。
  • 合成线程 (Compositor Thread): 负责接收主线程的图层和绘制信息,进行分块、协调 GPU 进行光栅化、最终将图层合成为屏幕图像并显示。它也负责处理滚动、缩放以及在独立合成层上的 transform 和 opacity 动画,实现流畅的交互体验。它是"执行者"和"优化者"。

这种分工使得即使主线程繁忙(例如在执行 JS),合成线程仍然可以响应用户的滚动操作或执行简单的动画,提升了页面的感知性能。


5. 回流、重绘与 Transform 性能再认识

  • 回流 (Reflow / Layout): 当元素的几何属性(如尺寸、位置、边距、边框)或页面结构发生变化,导致需要重新计算元素在页面上的位置和大小。回流是开销很大的操作,因为它可能影响其父节点、子节点乃至整个布局树。触发回流的操作一定会触发重绘。对应流水线的步骤 3 及之后
  • 重绘 (Repaint): 当元素的视觉样式(如颜色、背景、阴影)发生改变,但不影响其布局时,只需要重新绘制该元素(及其可能重叠的区域)。开销相对较小。对应流水线的步骤 5 及之后
  • 合成 (Compositing): 如果改变只影响独立合成层上的 transformopacity,浏览器可以跳过布局和绘制,直接在合成线程进行处理,开销最小。对应流水线的步骤 8

警惕强制同步布局 (Forced Synchronous Layout) / 布局抖动 (Layout Thrashing):

当你在 JavaScript 中修改了元素的样式(可能触发回流),然后立即读取需要依赖最新布局信息的属性(如 offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollLeft, scrollWidth, scrollHeight, clientTop, clientLeft, clientWidth, clientHeight, getComputedStyle(), getBoundingClientRect() 等),浏览器为了返回准确的值,不得不强制提前执行布局(回流)操作,打断了正常的异步流程。如果在循环中交替进行写操作和读操作,就会反复触发强制同步布局,导致性能急剧下降,这就是所谓的"布局抖动"。

优化策略: 尽量将读操作和写操作分离,先批量读取所需的值,再批量进行样式修改。


6. 属性值计算、包含块与优化建议

理解 CSS 属性值的计算过程和包含块的概念有助于优化样式:

  • 属性值计算阶段: CSS 值的确定经历多个阶段:声明值 -> 层叠值 -> 指定值 -> 计算值 (如 50% 根据包含块计算为 500px) -> 应用值 (考虑浏览器限制) -> 实际值。
  • 包含块 (Containing Block): 决定了元素(尤其是绝对定位和固定定位元素)的位置和尺寸计算基准。position 属性、display 属性等都会影响元素的包含块是谁(通常是最近的块级祖先、格式化上下文或视口)。

关键优化建议

  • 减少回流和重绘:
    • 避免频繁修改触发布局的样式。
    • 使用 transformopacity 实现动画,尽量触发合成。
    • 对需要频繁变动的元素,考虑使用 position: absolutefixed 将其"移出"正常文档流,减少对其他元素的影响。
    • 批量修改 DOM 或样式。
  • 合理使用图层:
    • 使用 will-change 属性提示浏览器为即将发生变化的元素准备独立图层,但不要滥用,过多图层会增加内存和管理开销。
    • 利用 Chrome DevTools 的 Layers 面板检查页面的分层情况,识别不必要的图层提升。
  • 避免布局抖动: 将样式读取和写入操作分离。
  • 监控性能: 持续使用 Performance 和 Layers 面板分析渲染瓶颈。

掌握浏览器渲染流水线的细节,是实现高性能 Web 应用的关键一步。