打开chrome浏览器的performance面板,记录页面加载的情况。大致有这么几个重要阶段:Loading、Scripting、Rendering、Painting。
Loading对应HTML等资源的加载,Scripting对应Javascript执行,Rendering对应Style的样式计算和Layout布局,Painting对应浏览器的栅格化、绘制和合成分层等启用GPU加速渲染的过程。
当然,上面这张图Scripting阶段占比比较高,如果想达到60FPS的流畅效果,根据RAIL规则,需要针对Scripting阶段进行优化。
这类文章有很多感兴趣的读者可以自行谷歌,我们今天详细说明Rendering阶段中的样式计算过程。
在进行样式计算之前,我们必须要有样式表。在Loading阶段浏览器除了解析HTML生成DOM之外还会进行CSS样式规则解析生成样式表。Web应用所有的样式规则应该在这个阶段全部注册到样式表内。为什么呢?稍后解答。
我们知道浏览器中DOM是运行时构建的,简单点说,DOM树的构建过程就是调用Node类的appendChild、insertBefore、removeChild这三个方法生成父子关系节点的过程,每个DOM节点都是一个Element实例,其构造函数继承关系如下:
从设计浏览器模型的角度来看,生成DOM节点并插入父节点的过程和样式计算的过程应该是相互独立、解耦合的。浏览器在实现上也确实是这样。
调用Document实例的createElement方法生成一个DOM节点,接着调用父节点的appendChild或insertBefore方法将DOM节点插入。紧接着DOM节点被传入到浏览器样式计算模块,在样式计算模块内根据样式表进行样式匹配和计算,样式计算完成后生成CSSOM节点插入到CSSOM树中。接下来就是大家熟悉的Layout布局和Paint绘制过程,这里不细表。
接下来举个例子,定义如下一个StyleSheet:
.class1 {
color: #ff0000;
}
.class2 {
color: #00ff00;
}
.class1 .class2 {
color: #0000ff;
}
span {
color: #f0f0f0;
}
HTML结构如下:
<div class="class1">
<div class="class3">
<span class="class2">一棠世界</span>
</div>
</div>
样式表生成一个类似以下结构的对象。
{
".class1": {
score: 10e6 + 1, // 代表权重
selector: "class", // 类选择器
def: {
color: "#ff0000"
},
name: ".class1"
},
".class2": {
score: 10e6 + 2, // 代表权重
selector: "class", // 类选择器
def: {
color: "#00ff00"
},
name: ".class2"
},
".class1 .class2": {
score: 2 * 10e6 + 3, // 代表权重
selector: "descendant", // 后代选择器
def: {
color: "#0000ff"
},
name: ".class1 .class2"
},
"span": {
score: 10e3 + 4, // 代表权重
selector: "tag", // 标签选择器
def: {
color: "#f0f0f0"
},
name: "span"
}
}
第一步,生成外层的div元素,并设置其class属性为class1。
在样式表中匹配到的样式规则如下:
[{
score: 10e6 + 1, // 代表权重
selector: "class", // 类选择器
def: {
color: "#ff0000"
},
name: ".class1"
}]
第二步,生成第二层的div,并设置其class属性为class3。未匹配到样式表中的规则。
第三步,生成第三层的span,并设置其class属性为class2。匹配到的样式规则如下:
{".class1 .class2": {
score: 2 * 10e6 + 3, // 代表权重
selector: "descendant", // 后代选择器
def: {
color: "#0000ff"
},
name: ".class1 .class2"
},
"span": {
score: 10e3 + 4, // 代表权重
selector: "tag", // 标签选择器
def: {
color: "#f0f0f0"
},
name: "span"
}
最后,针对返回的数组做一次合并,也就是CSS样式层叠。
这里要强调一下后代选择器的匹配过程,先匹配样式表中最末尾选择器是.class2的样式规则,找到.class2之前的选择器是.class1,那么在DOM树中以.class2为起点沿着父节点向上查找,直到找到.class1这个选择器。如果后代选择器较长,则查找的时间也随之增加。具体过程如下:
这里将模型简单化,以说明实现原理和过程。实际上,还存在内联样式和其他提升样式权重的选择器。同时浏览器中做了大量的算法和缓存优化,保证样式匹配的速度。
从上面这个例子可以看出,在实际的开发过程中,如果书写了大量的后代选择器,对样式计算的影响是非常大的。这里我建议开发者使用BEM方案解决,尽量减少选择器的长度。
前文阐述了第一次构建DOM树时的样式匹配过程。目前流行的前端框架会在运行时动态修改DOM的id或者class属性。这里请读者仔细思考一下,如果修改了某个元素的属性,那么沿着它的父节点向上所有节点的样式会受到影响吗?
答案是否定的。修改某个DOM的属性只会影响它本身以及后代元素的样式。当然这也发生在样式表中有例如`.class1 .class2`这种后代选择器规则的情况下。如果所有的选择器都是id选择器、类选择器、标签选择器这样的简单规则,那么每一次属性更新中,浏览器只需要做少量工作。
还有一种情况需要考虑。调用removeChild移除某个DOM节点,浏览器只需要在垃圾回收时移除对应的DOM。其子节点也一并移除,无需重新计算样式,这种情况是最简单也是最好理解的。
前面留下的问题还记得吗?在Loading阶段完成所有的样式规则的注册,不要在其他阶段动态添加样式。试想下,如果在其他阶段动态添加样式规则,那么之前DOM匹配到的样式就有可能被新的样式覆盖,浏览器无法保证每个节点的准确性就需要对所有的DOM节点重新进行样式计算。这显然也是一个非常大的性能损耗!
综上可以总结出几点针对样式计算阶段的CSS书写规范:
1、将应用中所有使用到的css样式规则统一在Loading阶段注册,不要动态注册样式。
2、尽量避免使用较长的后代选择器。
3、可以用BEM方案进行CSS模块化处理。
由于这是我的第一篇公众号文章,微信还没有给我开通评论功能。如果有读者希望继续深入了解相关知识,可以加我微信:xiaomuhenkaixin。或者扫描下面的二维码进入讨论群。