k韶华 2019-06-21
本文翻译自Inside a super fast CSS engine: Quantum CSS ,如果想要阅读原文,可以点击前往,以下内容夹杂本人一些思考,翻译也并不一定完全。
为什么翻译这篇文章尼,一开始只是好奇,基本在前端技术圈子混过都知道火狐正在用Rust语言开发新的浏览器引擎,作为前端开发对火狐的感情还是大大的有(虽然现在已经离不开chrome了),但是还是希望火狐能够再次引领Web的变革。
可以说前端这几年解决了前端工程化的很多痛点,但是性能这个坎依旧,期望webassembly尽快普及,但是对于前端必定又是一场腥风血雨,前端不会一直是现在这样的前端。既然webassembly出现了,那css怎么办,目前并没听说出现什么新的技术替代它(虽然它真的已经很不适合现代的前端了),那么只能开发一个新的引擎提高性能,这就是火狐家的量子引擎:Quantum CSS(又叫Stylo)。
这是火狐正在开发的Quantum项目,目的当然是为了让浏览器更快,从上图可以看得到各个模块,而Quantum CSS处于中间位置,这跟它在整个渲染过程中的位置一样,利用Rust可以相当有效利用现代处理器多核心的特性,能够几倍的提速。既然这么厉害,那从哪里可以体验尼:在火狐Nightly版进入about:config设置layout.css.servo.enabled 属性为 true就可以体验这吊炸天的引擎。
站在巨人的肩膀上,当然除了利用现代处理器的并行能力,还借鉴当前各家浏览器积累的一些优化技术,接下来会一一解析这些优化技术,如何让引擎更快。
CSS引擎是浏览器渲染引擎的一部分,而渲染引擎会把我们的HTML和CSS转换成屏幕上的像素(也就是画面吧)。
各家浏览器都会有自家的渲染引擎,例如谷歌的Blink,Edge的EdgeHTML,Safari的Webkit和火狐的Gecko,虽然有这么多引擎,但是他们都做着同样的事情:把HTML和CSS渲染成我们可以感知的界面。
而他们内部的工作需要:
综合后,我们可以知道CSS引擎开始计算样式时需要两样东西:DOM的节点树和一系列的样式规则。
CSS引擎会遍历所有DOM节点并计算每个节点所应用的样式,它会让DOM节点每个CSS属性都有一个值,就算你在样式表中并没有声明,它可能来自继承或者默认值,或者客户端的样式表(User Agent Style)。
可以认为引擎就像填表格一样,把这些最后计算出来的值一个一个填进去。
为了得到上面的表格,CSS引擎需要做两件事:
首先找出配置当前的节点的样式规则,放到一个list上去,这里也包括客户端的样式表。
然后会计算各个样式规则之间的权重,并且根据权重排序。
根据权重大小,得出最终应用的样式属性的值。
级联(The cascade)目的是为了让CSS更容易编写和维护,由于级联的存在,你可以在body上设置color属性,而li,p,span等元素可以直接使用同样的color,不需要每个元素都要去定义一次。
为了实现这个功能,CSS引擎会从表格里面寻找一些属性值仍然为空的值,如果属性默认是继承的话,CSS引擎会从父节点那里继承属性值,如果所有父节点都没有定义该属性值的话,就会使用默认的值。
现在我们的表格都填满了
上述表格的形式,只是一种表现方式,引擎真实的内部不是这样的。CSS拥有成千上百的属性,如果引擎为每个节点都生成这样一张表,会很快耗掉所有内存。
相反引擎内部通常会使用style struct sharing,样式的数据会集中在不同对象里面(style struct),然后使用指针指向这些对象。
这会很大程度上节省内存,因为各个节点间都很有可能拥有相似的属性值(例如兄弟节点间),另外因为很多属性也是通过继承获取的,所以父节点可以跟子节点间共享这些属性值。
如果我们不去优化这些工作,整个样式计算工作就会是这样:
这是巨量的工作,而且并不仅仅在页面加载的时候发生,它会随着用户的交互时刻都在存在(例如hover一个元素,CSS引擎需要从新计算样式)。
这样就意味着必须得去优化样式的计算工作,在过去20年,已经测试过不同的优化策略,而Quantum CSS则是组合利用这些最优的优化策略。
我们现在的CPU大多拥有多个核心,而Quantum CSS则会把不同DOM节点的样式计算工作分配到不同的核心上去,但是实现也有相当的难度,其中一个原因就是DOM节点树并不一定均匀的,这会导致其中一部分核心工作负荷比其他核心大。
为了让各个核心工作负荷更加合理,Quantum CSS使用了一种技术称作 work stealing,当一个DOM节点被处理的时候,引擎可以把它的子节点计算工作分成几个“work units”并且放进队列中。
当其中一个核心清空自身队列的工作后,它能够寻找其他队列上的其他work units然后执行,这意味着我们不需要提前就去分配好工作,在运行时也会达到最高的工作效率。
在大部分浏览器里面,很难让这种机制毫无错误的运行,而且CSS引擎本身就非常复杂,它在渲染引擎中两个最复杂的模块(DOM和layout)中间。这个过程非常容易产生bug,而且并行程序导致的bug非常难debug,可以通过这篇文章了解更多。
对于每个DOM节点,CSS引擎需要遍历所有样式规则去进行selector matching,且对于大部分节点这种matching并不会经常改变。例如,用户hover一个父节点,它的样式规则可能会改变,但是我们仍然需要重新子节点的样式规则去处理属继承的属性值,而子节点之前匹配的规则很有可能不会改变。
如果我们记录好子节点匹配哪些样式规则,而不用每次都进行一次selector matching,这可能会得到很大的优化。这就是所谓的rule tree,火狐前一个引擎所做的那样。
CSS引擎会遍历样式规则帮DOM节点找出匹配的选择器,然后根据权重排序,从而创建出一个样式规则的链表,然后将这个链表添加到rule tree中。
CSS引擎会尽可能利用已有的分支,为rule tree保持最少的分支数。
如果大部分链表中大部分的选择器,跟已存在的分支一样,引擎会顺着路径,除非它到达一个节点,rule tree并不存在一样分支,引擎就会添加一个新的分支。
在重新计算样式的过程中,引擎会快速检查父节点的改变是否会导致子节点匹配的样式规则改变。如果没有,子节点可以根据自己指向rule tree节点的指针计算样式。引擎会rule tree的节点往上查找,获取整个匹配的规则,从权重最大到权重最小的。这样就可以很轻松的跳过selector matching这一步了。
但是这样仍然还有很多工作要做,毕竟一个页面上节点成千上万,这时候并行计算的魔法又可以大显神威了。
由于整个页面的节点可能会有成千上万个,它们当中很多都匹配着相同的规则。例如wiki页面中每个p元素其实逗匹配着相同的样式规则,拥有一样的computed styles。
如果这里没有做优化,可能每个段落都要重新计算,但是如果有一种方法来证明每个段落的样式规则都是一样,引擎就只需计算一次就可以了。
这就是所谓的style sharing cache,由Chrome和Safari所发明的一种优化方式,在引擎处理一个节点后会把computed style放到cache里面,然后开始计算下一个节点的时候,引擎会先检查cache里面是否已经存在计算后的值。
而这些检查包括:
但是也有很多其他的情况,导致这些检查失效,例如:如果一个CSS规则使用了:first-child选择器,就算两个节点都已经符合上述的规则,结果也会是检查不通过。
在Webkit和Blink, style sharing cache在这些情况下会放弃检查并不会使用cache。由于大部分网站都使用了这些modern selectors(CSS3),这个优化的作用变得越来越少,所以Blink团队最近把它移除了。
在Quantum CSS,我们收集了所有这些怪异的选择器(CSS3)然后让它们加入检查。我们会把结果存储为0和1,如果两个元素拥有同样的0和1,那我们就知道他们是匹配同样的样式规则。
这样我们就可以继续享受style sharing cache带来的优化。
前半部分可以让我们知道CSS引擎的工作内容,后半部分让我们了解新引擎是如何优化性能的,真的学习了很多,我想你也一样。
background-color: blue;background-color: yellow;<input type="button" value="变蓝" @click="changeColorT