总览

● 从 Web 内容到屏幕像素的大致过程如下图:首先 Render 进程主线程(Render 线程)上的浏览器内核实例将 HTML 及关联的 CSS 信息转换为绘制信息,然后将绘制信息交给 Compositor 线程并由 GPU 进程协助完成到屏幕像素的转换(此过程称为光栅化<rasterize>);

● Render 线程和 Compositor 线程都位于浏览器的 Render 进程中。浏览器中进程与线程相关整理在这篇浏览器中的进程与线程
● Compositor 线程的”合成”名字源自现代浏览器实现光栅化使用的合成技术,Render 线程会将页面拆分为多个 layer ,各个 layer 将分别光栅化再合成为最终结果,这样做的优点是当页面滚动或实现动画时某些图层的上次光栅化结果可以直接使用;
● 渲染涉及初次渲染与更新渲染两种情况,更新渲染只有在必要的时候才进行(如 JS 操作 DOM/CSS、用户输入、异步加载、页面滚动与缩放、动画),更新渲染时会尽可能重用上一次渲染的结果;

初次渲染过程(以 Chrome 为例)

构建 DOM,Render 线程解析 HTML 生成 DOM 树,期间遇到的图片、CSS、JS 等外部资源文件会通知 Browser 进程从网络或缓存获取过来,当解析到 HTML 中的 JS 或者非延迟的 JS 文件加载完成时就暂停 DOM 解析先去执行完 JS 再返回;
构建 CSSOM,Render 线程再一次解析 HTML 收集各个来源(style 标签、css 文件、浏览器默认规则)的 style 规则构建成方便查找的 CSSOM 树结构;
构建 render 树,render 树来自 DOM 树与 CSSOM 树信息的合并,Render 线程遍历 DOM 树同时针对每个元素查找 CSSOM 树确定其最终 style(ComputedStyle)及当前元素是否渲染,构建出的 render 树中不包含 DOM 树中的不可见元素(如 script、meta),也不会包含 display 属性值为 none 的节点,但会包含伪元素,render 树的节点称为 RenderObject;

layout,Render 线程遍历 render 树构建 layout 树,layout 树的节点是 LayoutObject,LayoutObject 节点包含了对应 render 树节点的引用,以及经计算得出的该元素的 layout 信息(主要是 xy 坐标、所占页面空间大小等几何信息);
layer,Render 线程遍历 layout 树,将页面拆分成多个 layer 子树;
prepaint,Render 线程创建 property 树集合记录各 layer 子树的图形图像变换属性(transform、clip、effect、scroll),每个 layer 分别进入之后的 paint 阶段;

paint,Render 线程遍历 layer 子树创建描述其绘制过程的操作记录(称为 paint records)列表,由于操作记录的顺序会影响到元素重叠区域的前后遮挡关系,绘制过程会大致按照如下阶段创建绘制对象,于是每个元素通常对应多个绘制对象;

commit,Render 线程同步地将 layer 绘制对象列表及 property 树集合传输到 Compositor 线程,之后的步骤不再占用 Render 线程;
tiling,Compositor 线程将 layer 拆分为多个 tile,layer 所占区域可能比较大且距离当前视口较远,tile 按照优先级顺序进入 raster 阶段;
raster,GPU 进程中的 Raster 线程(Rasterizer Thread)们将各个绘制对象通过 Skia 库转换成 GL 函数的调用序列,GL 函数的调用序列执行后生成存储于 GPU 内存中的由像素颜色值组成的 bitmap;
draw,Compositor 线程收集当前视口区域内的所有 tile 的 GPU 内存地址和屏幕位置等信息(这些信息称为 draw quads)封装成一个 CompositorFrame 提交给 Browser 进程,除了 Render 进程之外,Browser 进程也同时收集来自 UI 线程、插件进程的 CompositorFrame,将它们一并交给 GPU 进程做聚合之后绘制到屏幕;

更新渲染过程(以 Chrome 中的 input 事件为例)

☆ 这里的 input 事件包括针对网页区域的所有点击、输入、滚轮、鼠标移动、鼠标悬停、触摸、缩放等鼠标键盘触摸事件;
☆input 事件首先被 Browser 进程捕捉到,Browser 进程判断事件的坐标及所在页面,将事件及坐标信息发送给对应的 Render 进程,事件先被 Compositor 线程处理,Compositor 线程判断事件是否位于事件监听区域,如果在监听区域之外则不经过 Render 线程而由 Compositor 线程直接生成新的 CompositorFrame,否则将事件交给 Render 线程,Render 线程处理事件流及事件监听回调,之后由 Event Loop 触发 Compositor 线程生成新 CompositorFrame 的过程;

要点补充

◇ 渲染流程中的每一步结果都是基于上一步结果的,所以当上游内容有变化时,下游步骤都需要重新执行一遍;
◇ reflow 与 repaint
(1)reflow 指浏览器需要重新计算元素的几何特性,从 layout 阶段重新生成一次 CompositorFrame,引起 reflow 的操作如:改变元素的 display 属性、DOM 树增减元素、改变元素的大小或位置,详细属性如下图;

(2)repaint 需要重新绘制元素,从 paint 阶段重新生成一次 CompositorFrame,引起 Repaint 的操作如:改变元素的背景色或 box-shadow;

(3)reflow 过程包含 repaint;
(4)repaint 会 repaint 变化元素及与其属于同一个 layer 的其他元素,不会 repaint 其他 layer;
◇ Compositor 线程里会维护当前帧与下一帧的绘制信息,当前帧(active tree)在 draw 阶段时可以同时进行下一帧(pending tree)的 raster 阶段;

网页渲染详细过程图

Chrome 相关工具

◇ 通过 Chrome 的 Layers 工具可以查看页面被分成了哪些 layer,以及每个 layer 的拆分原因和 paint 阶段的绘制对象;

◇ 通过 Chrome 的 Performance 工具可以查看渲染过程中 Render 线程、Raster 工作线程、GPU 进程、Compositor 线程的 timeline 及与浏览器各帧的对应关系;

◇ 目前的拆分 layer 过程及图形图像变换属性在 paint 阶段之前由 Render 线程完成,这从目前的 Performance 里可以看到,官方计划之后将把这部分工作放到 paint 阶段后面;

相关编程技巧

◆ 当页面元素变化时,通过避免 reflow 甚至同时避免 repaint 而只通过 Compositor 线程完成更新渲染,可以提高页面的性能,不仅因为总体运算量少,也因为 paint 之后的阶段都不在 Render 线程上进行;
◆ 当下主流浏览器(Chrome、Firefox、Safari、Opera)都实现了仅通过 compositing 完成 transform 与 opacity 两种 CSS 属性,极大提高了基于二者所实现页面动画(称为 compositing only animations)的流畅度,操作步骤如下:
(1)将动画元素提升到单独一层 layer,实现此目的的专用 CSS 属性”will-change”还在实验阶段,下图各方式可以更兼容地将元素提升为单独的 layer,例如:”transform: translateZ(0)”,这一步是下一步替代 CSS 属性的前提,即使下一步无法替代,layer 的提升也可以减少 reflow 与 repaint 的波及范围;

(2)尽可能使用 transform 代替 left、top 等属性以及使用 opacity 属性代替 rgba 颜色值来实现等价的 CSS 动画;

参考文献

life of a pixel
Inside look at modern web browser 系列
Constructing the Object Model
Render-tree Construction, Layout, and Paint
Chromium Source Code README.md
Eliminate content repaints with the new Layers panel in Chrome
High Performance Animations
Stick to Compositor-Only Properties and Manage Layer Count
GPU Accelerated Compositing in Chrome