之前提到过,凡是需要渲染主线程执行的任务,都要进入事件循环排队。当网络进程将 html 文档下载到本地后,会创建一个渲染任务进入事件循环,然后由渲染主线程开始渲染工作。
在浏览器的众多进程线程中,渲染核心相关的如下图。
整个渲染过程如下图所示。
下面我们介绍这些步骤。
解析 html 和 css
渲染的第一步就是将 html 文本解析成树结构,称为文档对象模型。
在浏览器中,通过 document 对象可以访问到 dom 树的各个节点。
同样地,css 代码也会被解析成树结构,称为 CSSOM。
StyleSheetList 是根节点,下面是多个 CSSStyleSheet 对象,每个 CSSStyleSheet 对应代码中的内部或外部样式表。
javascript
document.styleSheets // 根节点 StyleSheetList
我们可以用 JS 操作 CSSOM,从而修改或动态添加样式。
javascript
document.styleSheets[0].addRule('div', 'border: 1px solid #000') // 选择器和样式声明
值得一提的是,为了提高效率,浏览器会启动一个预解析线程快速找到 html 中的 css 链接并下载解析,如果是 JS 文件,则必须停下等待其下载且执行完毕,这是因为 css 解析不影响 html 解析而 JS 会影响。
样式计算
DOM 和 CSSOM 都准备好后,主线程的下一步是样式计算。其实就是计算每个 dom 节点的最终样式,这个最终样式会附着于该 dom 节点上,称为 computed style。
在 chrome 中我们可以查看一个节点的 computed style,也可以用 getComputed 方法获取。
javascript
getComputedStyle(document.body)
布局 Layout
现在我们有了一棵包含 computed style 的 dom 树,下一步是计算布局 ( layout )。
layout 过程就是根据 dom 树中每个节点的 computed style,计算节点的尺寸和位置。layout 的计算过程和视觉格式化模型息息相关。
dom 树和 layout 树不是一一对应的。比如 display: none 的节点不存在于 layout 树中;伪元素不存在于 dom 树,但会存在于 layout 树。
分层 Layer
分层类似图片编辑里的图层概念,主线程将它认为"经常一起变化"的节点生成一个新图层。
将哪些节点作为一个分层是浏览器决定的,我们也可以给浏览器一些提示,从而影响它的分层决策。跟堆叠上下文相关的 css 属性,比如 z-index、opacity、transform、will-change 会影响浏览器的分层结果。其中影响较大的是 will-change 属性。
css
.div {
will-change: transform; /* transform 属性会经常改变 */
}
以上属性提示浏览器,transform 属性会经常改变,浏览器会决定是否将这个 div 单独作为一层。
分层可以提高重新渲染的效率,但是分层需要占用较多内存,因此浏览器会对层数做取舍,兼顾内存和效率。
绘制 Paint
所谓绘制,就是为每个 layer 生成一系列绘制指令。绘制指令类似于 canvas 中的操作。事实上,canvas 就是浏览器将绘制功能暴露给开发者而来,也就是说,canvas 操作的就是绘制过程。
以下是绘制指令的一种方式。
makefile
Draw Rect
pos: x, y, w, h
color: red
到目前为止我们介绍的是渲染主线程的操作,目前屏幕上仍未得到像素点,只有一系列绘制指令,这些指令会交给合成线程处理。
分块 Tiling
GPU 擅长并行执行,为提高光栅化和画的效率,我们需要对 layer 进行分块,也就是将每个 layer 层分成多个小区域,这样有利于并行操作。
分块在合成线程中完成,它是渲染进程的线程。合成线程会在线程池中获取多个分块器(线程),它们同时工作,提升分块效率。
关于线程池,就是里面有多个创建好的线程,使用者可以申请其中空闲的线程使用,如果没有空闲线程,则排队等待。这是为了减少频繁创建回收线程的开销,同时控制线程的数量,避免创建太多线程。
光栅化 Raster
光栅化就是将每个 tile 变成位图,且为了提高体验,浏览器会优先处理靠近视口的 tile。
光栅化是在浏览器的 GPU 进程中执行的,GPU 进程内也会开启多个线程执行。
画 Draw
光栅化完成后,合成线程计算每个位图在屏幕上的位置(quad 信息),同时进行 css 的 transform 转换。这些信息会交给 GPU 进程,然后由 GPU 进程进行系统调用,从而交给显卡,最终呈现在屏幕上。
之所以要转交给 GPU 进程,是因为合成线程所在的渲染进程是在沙盒中运行的(安全策略),无法进行系统调用。
参考
How does browser work step by step [latest] --- rendering phase (part 3)