若干年前,我写过一篇介绍浏览器渲染流水线的文章 - How Rendering Work (in WebKit and Blink),这篇文章,一来部分内容已经过时,二来缺少一个全局视角来对流水线整体进行分析,所以打算重新写一篇新的文章,从一个更高抽象层次和高度简化的方式对浏览器的渲染流水线进行解析,能让大部分页端同学都能够看的明白,并以此作为指引来分析和优化页面的渲染/动画性能。
有些基本概念如图层,分块,光栅化基本没有发生变化,如果读者不理解的话请参考 How Rendering Work (in WebKit and Blink),本文不再过多解释。
本文基于当前版本的 Chrome 浏览器写成(60 左右),理论上部分知识可以应用于其它浏览器(当然术语会有一定差别)或者 Chrome 后续的版本,但是并不完全保证这一点。
1. 渲染流水线
上图显示了 Chrome 一个高度简化后的渲染流水线示意图:
- 最底层的是 Chrome 最核心的部分 Blink,负责JS的解析执行,HTML/CSS解析,DOM操作,排版,图层树的构建和更新等任务;
- Layer Compositor(图层合成器)接收 Blink 的输入,负责图层树的管理,图层的滚动,旋转等矩阵变幻,图层的分块,光栅化,纹理上传等任务;
- Display Compositor 接收 Layer Compositor 的输入,负责输出最终的 OpenGL 绘制指令,将网页内容通过 GL 贴图操作绘制到目标窗口上,如果忽略掉操作系统本身的窗口合成器,也可以简单认为是绘制在显示屏上;
当我们说 Compositor,在没有加修饰语的情况下,一般都是指 Layer Compositor。另外术语 Child Compositor(子合成器)也是指 Layer Compositor,相对于作为 Parent 的 Display Compositor 而言。
1.1 进程与线程
一个 Chrome 浏览器一般会有一个 Browser 进程,一个 GPU 进程,和多个 Renderer 进程,通常每个 Renderer 进程对应一个页面。在特殊架构(Android WebView)或者特定配置下,Browser 进程可以兼作 GPU 进程或者 Renderer 进程(意味着没有独立的 GPU 或者 Renderer 进程),但是 Browser 跟 Renderer,Browser 跟 GPU,Renderer 跟 GPU 之间的系统架构和通讯方式基本保持不变,线程架构也是同样。
- Blink 主要运行在 Renderer 进程的 Renderer 线程,我们通常会称之为内核主线程;
- Layer Compositor 主要运行在 Renderer 进程的 Compositor 线程;
- Display Compositor 主要运行在 Browser 进程的 UI 线程;
Display Compositor 未来应该会移到 GPU 进程的主 GPU 线程,当然对父子合成器进行调度的部分仍然是在 Browser 进程的 UI 线程。
1.2 帧
所有的渲染流水线都会有帧的概念,帧这个概念抽象描述了渲染流水线下级模块往上级模块输出的绘制内容相关数据的封装。我们可以看到 Blink 输出 Main Frame 给 Layer Compositor,Layer Compositor 输出 Compositor Frame 给 Display Compositor,Display Compositor 输出 GL Frame 给 Window。我们觉得一个动画是否流畅,最终取决于 GL Frame 的帧率(也就是目标窗口的绘制更新频率),而觉得一个触屏操作是否响应即时,取决于从 Blink 处理事件到 Window 更新的整个过程的耗时(理论上应该还要加上事件从 Browser 发送给 Compositor,再发送给 Blink 的这个过程的耗时)。
1.1.1 Main Frame
Main Frame 包含了对网页内容的描述,主要以绘图指令的形式,或者可以简单理解为某个时间点对整个网页的一个矢量图快照(可以局部更新)。当前版本的 Chrome,图层化的决策仍然由 Blink 来负责,Blink 需要决定如何根据网页的 DOM 树来生成一颗图层树,并以 DisplayList 的形式记录每个图层的内容(未来图层化决策应该会转移到 Layer Compositor,Blink 只输出 DisplayList 树和 DisplayList 节点的关键属性,同时 DisplayList 不再以图层作为单位,而是以每个排版对象作为单位)。
图层化决策一般由以下几个因素决定:
- 特殊元素如 Plugin,Video,Canvas(WebGL);
- 维护正确的层级关系来保证绘制顺序是正确的,比如 Overlap 的计算;
- 减少图层树的结构变更,减少图层内容的变更(目前 Blink 网页内容的变更是以图层为原子单位的,如果以一个元素为根节点生成图层,该元素的某些 CSS 属性如 Transform 的变更不会引起所属图层内容的变更);
第三点是可以被页端所直接控制来优化图层结构及 Main Frame 性能,像传统的 translate3d hack 和新的 CSS 属性 will-change。
1.2.2 Compositor Frame
Layer Compositor 接收 Blink 生成的 Main Frame,并转换成合成器内部的图层树结构(因为图层化决策仍然由 Blink 负责,所以这里的转换基本上可以认为是生成一棵同样的树,再对逐个图层的进行拷贝)。
Layer Compositor 需要为每个图层进行分块,为每个分块分配 Resource(Texture 的封装),然后安排光栅化任务。
当 Layer Compositor 接收到来自 Browser 的绘制请求时,它会为当前可见区域的每个图层的每个分块生成一个 Draw Quad 的绘制指令(矩形绘制,指令实际上指定了坐标,大小,变换矩阵等属性),所有的 Draw Quad 指令和对应的 Resource 的集合就构成了 Compositor Frame。Compositor Frame 被发送往 Browser,并最终到达 Display Compositor(未来也可以直接发给 Display Compositor)。
1.2.3 GL Frame
Display Compositor 将 Compositor Frame 的每个 Draw Quad 绘制指令转换一个 GL 多边形绘图指令,使用对应 Resource 封装的 Texture 对目标窗口进行贴图,这些 GL 绘图指令的集合就构成了一个 GL Frame,最终由 GPU 执行这些 GL 指令完成网页在窗口上占据的可见区域的绘制。
1.3 调度
Chrome 渲染流水线的调度是基于请求和状态机响应,调度的最上级中枢运行在 Browser UI 线程,它按显示器的 VSync(垂直同步)周期向 Layer Compositor 发出输出下一帧的请求,而 Layer Compositor 根据自身状态机的状态决定是否需要 Blink 输出下一帧。
Display Compositor 则比较简单,它持有一个 Compositor Frame 的队列不断的进行取出和绘制,输出的频率唯二地取决于 Compositor Frame 的输入频率和自身绘制 GL Frame 的耗时。基本上可以认为 Layer Compositor 和 Display Compositor 是生产者和消费者的关系。