目录
- 渲染时间点
- 渲染流水线
- 1,解析(parse)HTML
- 1.1,DOM树
- 1.2,CSSOM树
- 1.3,解析时遇到 css 是怎么做的
- 1.4,解析时遇到 js 是怎么做的
- 2,样式计算 Recalculate style
- 3,布局 layout
- 4,分层 layer
- 5,绘制 paint
- 6,分块 tiling
- 7,光栅化 raster
- 8,画 draw
- 常见面试题
- 什么是 reflow
- 什么是 repaint
- 为什么 transform 效率高
在上一篇文章中,介绍了 浏览器的事件循环,其中提到了浏览器的进程模型。那浏览器是如何渲染页面的呢?
渲染时间点
浏览器会通过网络进程中的线程来通信,获取到 html 数据后生成渲染任务,发送给消息队列。
渲染主线程会执行渲染任务。整个渲染流程:把 html 字符串解析为像素点信息,再交给 GPU来渲染后在页面中展示。
渲染流水线
每个阶段都有明确的输入输出,上个阶段的输出会成为下个阶段的输入。形成一套完整的流水线。
1,解析(parse)HTML
会将 html 字符串解析为 2个树。因为字符串不好操作,对象更容易处理。
1.1,DOM树
也就是 document
对象。可以在控制台通过console.dir(document)
展示对象结构。
1.2,CSSOM树
包括:
<style>
内部样式<link>
外部样式style=""
内联样式- 浏览器默认样式表
注意,这里的 CSSOM树 ≠ document.styleSheets
。因为 document.styleSheets
只包括内部样式和外部样式,每写一个 <style>
或 <link>
就会多一个 CSSStyleSheet
样式表:
举例说明:
<html>
<head>
<style>
body h1 {
color: red;
font-size: 3em;
}
div p {
margin: 1em;
color: blue;
}
</style>
</head>
<body>
<h1>下雪天的夏风</h1>
<div>
<p>求关注</p>
</div>
</body>
</html>
可以看到:
CSSStyleSheet
样式表CSSStyleRule
规则对象selectorText
选择器style
样式(键值对)
另外,CSSStyleSheet
样式表是可以直接通过 js 操作的。
举例:通过 js 给页面所有 div 添加 border
document.styleSheets[0].addRule('div', 'border: 1px solid !important')
这样添加样式的方式,一般框架用的多。最终样式不会在内联样式中展现。
1.3,解析时遇到 css 是怎么做的
渲染主线程遇到 css 时,会启动一个预解析线程,让它来率先下载和解析 css。渲染主线程继续解析 html。
预解析线程会快速浏览,如果遇到外部样式link
,会通知网络线程来下载 css,下载完成后进行“解析”完成后交给渲染主线程。
并不是真正的解析,只是做一些前期工作,最终生成 CSSOM 树还是由渲染主线程来完成。
所以,css 代码不会阻塞解析 HTML。
1.4,解析时遇到 js 是怎么做的
没有生成所谓的 js 树,是因为 js 只需要执行一遍即可。DOM树和CSSOM树作为解析 HTML 的输出,后续还会有其他的操作。
渲染主线程遇到内部 js 时,直接启动 V8 引擎执行即可;遇到外部 js 时,会启动一个预解析线程,让它来下载 js,渲染主线程暂停。
预解析线程会通知网络线程来下载 js,下载完成后再交给渲染主线程来执行。执行完继续解析 HTML。
这样做的原因:DOM 树是边解析边生成的,而 js 代码可能会修改之前已解析好的内容。
所以,js 代码会阻塞解析 HTML。
2,样式计算 Recalculate style
遍历DOM树,计算每个节点的最终样式 Computed Style。
这一过程,许多预设值会变成绝对值,比如 red
变为 rgb(255,0,0)
;相对单位变为绝对单位,比如rem
变为 px
最终会得到1个带有最终样式的 DOM 树。
可以在浏览器的 computed 窗口中,或使用 getComputedStyle() 查看某个元素的最终样式。
3,布局 layout
遍历DOM树的每个节点,根据 css 属性值计算每个节点的几何信息(尺寸,相对包含块的位置),生成一个 layout 树。
注意到 DOM 树和 layout 树不一定一一对应。
举例1:diaplay:none
的元素不会出现在 layout 树中。
问题来了,为什么<head>
<link>
等元素默认是隐藏的?因为在浏览器默认样式表中,它们 diaplay:none
!
举例2:伪元素的 content
内容在 DOM 树中没有,在 layout 树中有。
举例3:内容必须在行盒中,行盒和块盒不能相邻。所以在 layout 树中会有匿名块盒。
解释:“行级元素”,“块级元素”这些元素指的是 html。但元素的类型是 css 属性决定的。所以称为行盒或块盒。
4,分层 layer
现在 layout 布局树中每个节点的几何信息,尺寸位置等都明确了。渲染主线程会使用一套策略对整个布局树分层。
目的是提升效率,这样可以让之后页面的修改更新不会影响到其他层。类似 PS 中的图层,修改某一个图层不会影响到其他图层。
可以在浏览器控制台的
Layers
面板查看当前网页的分层信息。
也不会分太多的层,因为会比较占内存。滚动条是单独一层。
另外,和堆叠上下文有关的 css 属性(transform,opacity)会影响分层的决策。其中 will-change
属性能更大程度的影响分层角色,可能会将设置该属性的元素单独分一层。
因为这个属性会告诉浏览器,我可能会经常变化,浏览器最好掂量下。
5,绘制 paint
分层后,会对每层都生成绘制指令,类似于 canvas 中的 API 一样。其实canvas 用的就是浏览器内核的绘制功能。
指令举例:“笔”移动到 xx 坐标位置,画 100*100 的矩形,并用红色填充等等。
以上。渲染主线程的工作结束,剩下的步骤交给其他线程来完成。
6,分块 tiling
将每层都分为多个小的区域,浏览器视窗区域的优先级最高,靠近视窗区域的优先级次之。
分块逻辑:渲染主线程每个图层的绘制信息交给合成线程,合成线程又会启动多个分块线程来对每个图层进行分块。
合成线程也属于渲染线程
7,光栅化 raster
将每个块变成位图,位图就是每个像素点的信息。优先处理靠近视窗的块。
位图就是内存中的二位数组,其中记录了每个像素点的信息。
此过程会用到GPU来加速,用到显卡。
8,画 draw
合成线程现在拿到了所有的信息,在画之前还需要确认【指引信息 quad】,也就是位图相对的屏幕在哪里。
注意,之前布局树中的信息是相对于整个页面的。现在需要知道每个块相对于屏幕的位置信息。
步骤:合成线程将指引信息 --> GPU 进程 --> 硬件显卡,由显卡来呈现最终的像素信息。
GPU 做中转的原因是:GPU 是浏览器的进程。合成线程属于渲染进程,它是在沙盒中的,与硬件系统做隔离,提升安全性。所以渲染进程是没有调度系统硬件能力的。
而 css 中的 transform 属性就是在这一步完成的。这就是 transform 效率高的原因,直接跳过之前所有的步骤。
常见面试题
什么是 reflow
【recalculate layoutBlockFlow 重排】它的本质是重新计算 layout 树。
当做了会影响 layout 树的操作后,比如修改几何尺寸相关的信息时,会引起重新计算 layout 树。
并且,为了避免连续多次的操作导致 layout 树反复计算,浏览器会合并这些操作,当 js 代码完成后统一计算。所以这一步骤是异步的。
同样因为这个原因,当 js 获取布局属性时,可能无法获取到最新的布局信息。
浏览器会在反复权衡下,最终决定获取属性时立即 reflow。
什么是 repaint
它的本质是重新根据分层信息计算了绘制指令。
当改变了可见样式,比如颜色等和几何尺寸无关的属性时,就需要重新计算,会从【绘制】这一步骤开始重新执行。
而因为几何尺寸也属于可见样式,所以 reflow 一定会引起 repaint。
为什么 transform 效率高
因为 transform 既不会影响布局,也不会影响绘制,它只会影响渲染的最后一步【画】。而【画】是在合成线程中,不会影响到渲染主线程。同样无论渲染主线程多忙,也不会影响到 transform。
验证代码如下,当渲染主线程卡死时,transform 不受影响。
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.common {
width: 50px;
height: 50px;
background-color: salmon;
border-radius: 50%;
margin: 10px;
}
.ball1 {
animation: move1 1s alternate infinite;
}
.ball2 {
position: relative;
left: 0;
animation: move2 1s alternate infinite;
}
@keyframes move1 {
to {
transform: translate(100px);
}
}
@keyframes move2 {
to {
left: 100px;
}
}
</style>
</head>
<body>
<button id="btn">死循环3s</button>
<div class="common ball1"></div>
<div class="common ball2"></div>
<script>
btn.addEventListener("click", function () {
delay(3000);
});
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
</script>
</body>
</html>
效果:
滚动也是一样的逻辑,如果 js 有一段上面的死循环,并不会影响到滚动。因为只有视窗内元素的位置变了,直接执行【画 draw】这一步骤。
以上。
参考:渡一教育。