渲染器进程接收到的数据也就是html. 渲染器进程的核心任务就是把html, css, js, image等资源渲染成用户可以交互的web页面.
MDN上的图:
本文用到的图:
构造 DOM Tree.
在DOM树构造过程中会创建document对象, 然后以document为根节点的DOM树不断进行修改, 向其中添加各种元素. html代码中往往会引入一些额外的资源, 比如说图片, css, js等. 图片和css这些资源需要通过网络下载或者从缓存中直接加载. 这些资源不会阻塞html的解析. 因为他们不会影响DOM的生成. 但当HTML标解析过程中遇到script标签, 就会停止html解析流程, 转而去加载解析并且执行js(因为js可能会改变当前页面的HTML结构).
添加 style.
在html解析完毕后, 我们会得到一个 DOM Tree. 但我们还不知道它的每个节点应该长什么样子. 主线程需要解析css, 并确定每个DOM节点的计算样式(element.style
). 即使你没有提供自定义的css的样式, 浏览器会有自己默认的样式表.
构造 Layout Tree. 确定每个节点的位置和大小.
主线程通过遍历dom和计算好的样式来生成 Layout tree. Layout tree 上的每个节点都记录了x,y坐标和边框尺寸.
DOM Tree 和 Layout Tree 并不是一一对应的. 设置了display:none
的节点不会出现在 Layout Tree 上. 而在before
伪类中添加了content
值的元素, content
里的内容会出现在 Layout Tree里, 不会出现在 DOM Tree 里. 这是因为DOM是通过HTML解析获得, 并不关系样式. 而Layout Tree是根据DOM和计算好的样式来生成. Layout Tree是和最后展示在屏幕上的节点是对应的.
创建Paint Record. 确定绘制节点的顺序.
举例来说, z-index
属性会影响节点绘制的层级关系. 如果我们按照dom的层级结构来, 则会导致错误的渲染. 为了确保在屏幕上展示正确的层级, 主线程遍历 Layout Tree 创建一个绘制记录表(Paint Record)该表记录了绘制的顺序. 这个阶段被称为绘制(paint).
构造 Layer Tree. 把像素点显示在屏幕上.
栅格化不占用主线程, 只占用合成器线程和栅格线程.
chrome早期只栅格化用户可视区域(Viewport)的内容. 当用户滚动页面时, 再栅格化更多的内容来填充缺失的部分. 这种方式带来了一个问题: 展示延迟.
随着不断地优化升级, 现在的chrome使用了一种更为复杂的栅格化流程, 叫做合成(Composting). 合成是一种将页面的各个部分分成多个图层分别对其进行栅格化, 并在 合成器线程(Compositor Thread) 中单独进行合成页面的技术. 简单来说就是页面所有的元素按照某种规则进行分图层, 并把图层都栅格化好. 然后只需要把可视区的内容组合并成一帧展示给用户即可.
首先, 主线程遍历Layout Tree生成Layer Tree(图层树).
当Layer Tree生成完毕和绘制顺序确定后, 主线程将这些信息传递给合成器线程, 在栅格化之前对图层进行切分. 由于一层的长度可能和页面的长度一样, 因此合成器将他们切分为许多图块(tiles), 然后将这些图块分组发送给多个栅格化线程(Raster Thread), 将他们栅格化, 并存储在GPU内存中.
当图块栅格化完成后, 主线程将Layer Tree和Paint Record一起传给合成器线程, 合成器线程按照规则进行分图层, 并把图层分为更小的图块传给栅格化线程进行栅格化.
栅格化完成后, 合成器线程会获得栅格线程传过来的”draw quads“图块信息.
根据这些信息合成器线上合成了一个合成器帧, 然后将该合成器帧通过IPC传回给浏览器进程, 浏览器进程再传到GPU进行渲染. 之后就展示到你的屏幕上了.
当涉及到DOM节点的布局属性(如 height)发生变化时,就会重新计算该属性,浏览器会重新描绘相应的元素,此过程叫重排(Reflow)。
重排会重新进行样式计算(style computing)及其后面的所有流程.
当影响DOM元素可见性的属性发生变化(如 color)时, 浏览器会重新描绘相应的元素, 此过程称为 重绘(Repaint)。因此重排必然会引起重绘。
重绘不会重新触发布局(layout 和 layer), 但还是会触发样式计算(style)和绘制(paint).
重排和重绘都是运行在主线程上. 而 js 也是运行在主线程上.(一个event loop里有多个task queue, task queue里的被调度的两个相邻task之间, 也就是 micro tasks 执行完毕之后, 下一个task执行之前, 会进行重新渲染. 渲染和task的执行都在主线程上) 于是就会出现抢占执行时间的问题. 如果你写了一个不断导致重排重绘的动画, 浏览器则需要在每一帧(frame)都运行样式计算布局和绘制的操作.
当页面以每秒60帧的频率刷新时, 才不会让用户感受到页面卡顿. 如果你在运行动画时还有大量的js任务需要执行, 因为布局, 绘制和js执行都发生在主线程, 挡在一帧的时间内布局和绘制结束后, 如果还有剩余时间, js就会拿到主线程的使用权. 如果js执行时间过长, 就会导致在下一帧开始时JS没有及时归还主线程, 导致下一帧动画没有按时渲染, 就会出现页面动画的卡顿.
一种优化手段是借助于requestAnimationFrame()
方法. 这个方法会在每一帧被调用, 通过 API 的回调, 然后我们可以把 js 运行任务分成一些更小的任务块(分到每一帧),在每一帧时间用完前暂停 js 执行. 这样的话, 在下一帧开始时, 主线程就可以按时执行布局和绘制. React 的最新渲染引擎, React Fiber, 用到了api来做了很多优化.
还有第二种优化手段. 通过刚才流程图, 我们知道栅格化的整个流程是不占用主线程的. 只在合成器线程和山歌线程中运行. 这就意味着它无需和 js 抢夺主线程. 我们刚才提到, 如果反复进行重绘和重排, 可能会导致掉帧. 这是因为有可能 js 执行阻塞了主线程. 而 css 中有个动画属性叫 transform
, 通过该属性实现的动画不会经过布局和绘制, 而是直接运行在合成器线程和栅格线程, 所以不会受到主线程中js执行的影响. 更重要的是通过transform
,实现的动画由于不需要经过布局绘制, 样式计算等操作, 所以节省了很多运算时间(方便实现负责的动画). 我们常常实现的动画效果如位置变化, 宽高变化, 旋转, 3D等, 都可以使用transform
来代替.
相关资料:
[干货]浏览器是如何运作的
The Chromium Projects
Design Documents
Render-Tree Construction, Layout, and Paint
Tasks, microtasks, queues and schedules