渲染就是从 HTML 文档到像素的过程,主要由渲染进程的渲染主线程和合成线程实现。
网络线程通过地址栏的 url 获取对应的 HTML 文档,并生成渲染任务加入渲染主线程的消息队列。 通过事件循环,渲染主线程取出渲染任务并进行处理,即开始执行渲染步骤。
解析 HTML
将 HTML 文档解析为 DOM 树和 CSSOM 树,根节点的接口分别为 document
和 document.styleSheets
。
预解析线程
和渲染主线程几乎同时启动,负责快速浏览 HTML 文档,查找外链 CSS 和 JS 文件。 找到后,通知网络线程提前下载,并对 CSS 文件进行预解析,最后发送给渲染主线程。
- 如果渲染主线程解析到外链 CSS 时文件还没下载完,则跳过。
- 如果渲染主线程解析到外链 JS 时文件还没下载完,则等待文件下载,在执行 JS 完后才继续解析 HTML。这样设计的原因是 JS 可能会修改 DOM 树。
样式计算
通过 DOM 树和 CSSOM 树计算每个节点的最终样式,可以通过 getComputedStyle
获取。主要包含 2 个过程:
- css 属性值计算
- 层叠
- 继承
- 预设值变绝对值:
red
-->rgb(255, 0, 0)
- 相对单位变绝对单位:
em
-->px
- 视觉格式化模型
- 盒模型
- 包含块
布局
根据样式计算每个节点的尺寸和位置,生成布局树。DOM 树和布局树并不一一对应:
- 如果一个节点样式存在
display: none
,该节点及其子节点不会出现在布局树中 - 伪元素节点不出现在 DOM 树种,而出现在布局树中
- ...
clientHeight
、offsetHeight
属于布局属性。
重排 reflow
reflow 的本质是重新计算布局树。当 JS 执行了影响布局树的操作后,则引发 reflow。
具体情况是,这些影响布局树的操作会包装为一个布局任务加入消息队列,等到 JS 全部执行完后再 reflow。 但是这样会导致获取节点布局属性时,无法获取 reflow 后的值,所以浏览器决定在获取布局属性时立即执行 reflow。
分层
分层是为了细化绘制单位,避免过度重绘导致的性能损耗。
- 滚动条专门在一个层中
- 堆叠上下文相关的 css 属性将影响分层
INFO
可以打开 Devtools 的 Layers 面板查看。
绘制
为每个层生成绘制指令集,这是渲染主线程的最后一个任务。
重绘 repaint
reflow 的本质是重新生成绘制指令。改动了可见样式后,则引发 repaint。
requestAnimationFrame
的回调将在下一次 repaint 前运行,因为回调可能更改节点。
分块
将每个层分为多个小区域,以便后续优先画出靠近视口区域的块。 渲染主线程将各层的指令集交给合成线程,合成线程会从线程池中获取多个线程完成分块工作。
光栅化
将每个块变成位图,可以使用 GPU 加速。 合成线程将每个块交给 GPU 进程,GPU 进程处理完后把位图返回给合成线程。
画
合成线程计算出每个位图在屏幕上的位置,然后交给 GPU 进程呈现。
INFO
css 的 transform
属性在这一步被解析,这就是 transform
动画性能表现优异的原因。