浏览器的渲染流程(2)
布局
前面这些步骤完成之后,渲染进程就已经知道页面的具体文档结构以及每个节点拥有的样式信息了,可是这些信息还是不能最终确定页面的样子。
举个例子,假如你现在想通过电话告诉你的朋友你身边的一幅画的内容:“画布上有一个红色的大圆圈和一个蓝色的正方形”,单凭这些信息你的朋友是很难知道这幅画具体是什么样子的,因为他不知道大圆圈和正方形具体在页面的什么位置,是正方形在圆圈前面呢还是圆圈在正方形的前面。
渲染网页也是同样的道理,只知道网站的文档流以及每个节点的样式是远远不足以渲染出页面内容的,还需要通过布局(layout)来计算出每个节点的几何信息(geometry)。
生成布局树的具体过程是:主线程会遍历刚刚构建的 DOM 树,根据 DOM 节点的计算样式计算出一个布局树(layout tree)。布局树上每个节点会有它在页面上的 x,y 坐标以及盒子大小(bounding box sizes)的具体信息。
布局树大部分时候,和 DOM 树并非一一对应。虽然它长得和先前构建的 DOM 树差不多,但是不同的是这颗树只有那些可见的(visible)节点信息。
比如 display:none 的节点没有几何信息,因此不会生成到布局树;
又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。
还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
所以,这也是为什么不要直接在HTML里面直接写内容,无论怎么样,我们都需要用一个标签将其包裹
分层
在确认了布局树后,接下来就是绘制了么?
还不急,这里还会有一个步骤,就是分层。
分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
为了确定哪些元素需要放置在哪一层,主线程需要遍历整颗布局树来创建一棵层次树(Layer Tree)
滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过使用 will-change 属性来告诉浏览器对其分层。
生成绘制指令
分层工作结束后,接下来就是生成绘制指令。
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
这里的绘制指令,类似于“将画笔移动到 xx 位置,放下画笔,绘制一条 xx 像素长度的线”,我们在浏览器所看到的各种复杂的页面,实际上都是这样一条指令一条指令的执行所绘制出来的。
如果你熟悉 Canvas,那么这样的指令类似于:
context.beginPath(); // 开始路径
context.moveTo(10, 10); // 移动画笔
context.lineTo(100, 100); // 绘画出一条直线
context.closePath(); // 闭合路径
context.stroke(); // 进行勾勒
但是你要注意,这一步只是生成诸如上面代码的这种绘制指令集,还没有开始执行这些指令。
另外,还有一个重要的点你需要知道,生成绘制指令集后,渲染主线程的工程就暂时告一段落,接下来主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
分块
合成线程首先对每个图层进行分块,将其划分为更多的小区域。
此时,它不再是像主线程那样一个人在战斗,它会从线程池中拿取多个线程来完成分块工作。
光栅化
分块完成后,进入光栅化阶段。所谓光栅化,就是将每个块变成位图。
更简单的理解就是确认每一个像素点的 rgb 信息,如下图所示:
光栅化的操作,并不由合成线程来做,而是会由合成线程将块信息交给 GPU 进程,以极高的速度完成光栅化。
GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
绘制
最后一步,我们总算迎来了真正的绘制。
当所有的图块都被栅格化后,合成线程会拿到每个层、每个块的位图,从而生成一个个「指引(quad)」信息。
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这就是 transform 效率高的本质原因。
合成线程会通过 IPC 向浏览器进程(browser process)提交(commit)一个渲染帧。这个时候可能有另外一个合成帧被浏览器进程的 UI线程(UI thread)提交以改变浏览器的 UI。这些合成帧都会被发送给 GPU 完成最终的屏幕成像。
如果合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给 GPU 来更新页面。
最后总结一下浏览器从拿到 html 文档到最终渲染出页面的整体流程,如下图:
常见面试题
- 什么是 reflow?
reflow 的本质就是重新计算 layout 树。
当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。
也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。
浏览器在反复权衡下,最终决定获取属性立即 reflow。
- 什么是 repaint?
repaint 的本质就是重新根据分层信息计算了绘制指令。
当改动了可见样式后,就需要重新计算,会引发 repaint。
由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。
- 为什么 transform 的效率高?
因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段
由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。
Comments