火狐的超快速 CSS 引擎:Quantum CSS(三)

### 全都并行运行 Servo 项目(Quantum CSS 来自于此)是一个实验性的浏览器,它试图把网页渲染的所有部分并行化。 这是什么意思呢? 计算机就像大脑。 一部分负责思考(算术逻辑单元/arithmetic logic unit, ALU)。 紧邻着有一些短期记忆体(寄存器)。 它们在 CPU 上组合在一起。 还有更长期的记忆体,RAM(随机存取存储器/Random Access Memory, RAM)。 ![](/uploads/article/2017/08/26/20170826235840_2377.webp) 早期的计算机使用这种 CPU 仅能同时思考一件事。 但是过去的十多年里,CPU 变为有多个 ALU 和寄存器,在 CPU 核心上组合在一起。 这样 CPU 可以同时思考几件事——并行。 ![](/uploads/article/2017/08/27/20170827000044_8225.webp) Quantum CSS 利用这些最新计算机特性,将不同 DOM 节点的样式计算分配给不同核心。 看起来很简单…… 只要把树按分支拆分,在不同核心上运行。 实际上更难一些,原因很多。 其中一个原因是 DOM 树经常是不均匀的。 其中一个核需要比其它核做更多的工作。 ![](/uploads/article/2017/08/27/20170827000307_9430.webp) 为了平衡工作,Quantum CSS 使用一种叫做工作窃取(work stealing)的技术。 一个 DOM 节点在处理时,代码将它的直属子节点拆分为 1 个或多个“工作单元”。 这些工作单元被放进队列。 ![](/uploads/article/2017/08/27/20170827000428_3260.webp) 当一个核心完成了队列里的工作,它可以在其它队列里获得更多的工作。 意味着我们可以均匀的分配工作,而不需要提前遍历整个树去计算如何平衡。 ![](/uploads/article/2017/08/27/20170827000802_7352.webp) 大多数浏览器里,很难正确实现并行。 并行是已知的难题,CSS 引擎非常复杂。 并且处于渲染引擎中最复杂的两个部分之间——DOM 和布局。 所以很容易产生错误,并行化可能会导致很难追踪的错误,称为数据竞争。 如果成百上千的工程师都在贡献代码,并行化编程如何能不担心? 这是我们引入 Rust 的目标。 ![](/uploads/article/2017/08/27/20170827001036_1013.webp) 使用 Rust,可以静态验证确认没有数据竞争。 只要一开始不让它们进入代码,就可以避免棘手的调试错误。 编译器不允许你这么做。 这样,CSS 样式计算变成了并行问题——没有什么能阻止你高效的并行运行。 也意味着可以获得线性的加速效果。 如果机器有 4 个核心,可以接近以 4 倍速度运行。 ### 用规则树(Rule Tree)加速样式重建(restyle) 每个 DOM 节点,CSS 引擎需要遍历全部规则来进行选择器匹配。 对于大多数节点,匹配不经常变化。 例如,如果用户将鼠标悬停在父节点,它匹配的规则可能会变化。 仍然需要重计算它的后代节点的样式来解决属性继承,但是后代节点匹配的规则可能并没有变化。 所以最好记下哪个规则匹配了这些后代节点,这样就不需要再次对它们做选择器匹配了…… 这就是规则树(借鉴了 Firefox 前一代 CSS 引擎)做的事。 CSS 引擎经过一个过程,找到那些可匹配的选择器,然后对其按照优先级排序。 这时,它就建好了规则链表。 该列表会添加到规则树中。 ![](/uploads/article/2017/08/27/20170827001214_7046.webp) CSS 引擎尝试将树中的分支数量保持最小。 为此,它将尽可能的复用分支。 如果一个列表中的大多数选择器与已有分支相同,则沿用同样的路径。 但是可能有一个点,列表中的下一个规则不在这个分支上。 只在这一点才添加新分支。 ![](/uploads/article/2017/08/27/20170827001333_3081.webp) DOM 节点得到一个指针,指向最后添加的规则(本例中,dev#warning 那条规则)。 这是最优先的一条。 样式重建时,引擎先迅速检查父节点的改变是否可能改变子节点匹配的规则。 如果不改变,对于任何后代,引擎可以直接根据后代节点的指针找到那条规则。 从那条规则,它能沿着规则树向上找回根,得到匹配规则的完整列表,从最高优先级到最低优先级。 也就是说可以完全跳过选择器匹配和排序的过程。 ![](/uploads/article/2017/08/27/20170827001507_9877.webp) 这有助于减少样式重建过程的工作量。 但是初始化样式时仍然有很多工作。 如果有 10,000 节点,仍然需要做 10,000 次选择器匹配。 有另一种方法来加速。 ### 使用样式共享缓存(Style Sharing Cache)加速初始渲染(和层叠) 考虑有成千上万个节点的页面。 很多节点匹配相同规则。 例如,一个很长的 Wikipedia 页面…… 主内容区域内的段落最终应该匹配完全相同的规则,具有完全相同的计算样式。 如果不做优化,CSS 引擎必须对每个段落匹配选择器并计算样式。 但是如果有办法证明段落与段落的样式都相同,引擎就可以只做一次工作,把每个段落节点指向同一计算样式。 这就是样式共享缓存(受 Safari 和 Chrome 启发)的做法。 一旦处理完一个节点,就将计算出的样式放入缓存。 然后,在计算下一个节点的样式之前,它会运行几个校验来看是否能使用缓存。 这些校验包括: - 两个节点是否有相同 id、class 等? 如果有,有可能匹配同一规则。 - 任何不是基于选择器的——如内联样式——节点是否有相同的值? 如果是,以上的规则或者不被覆盖,或者以相同的方式覆盖。 - 两者的父节点指向计算出的同一样式对象? 如果是,继承的值也将相同。 ![](/uploads/article/2017/08/27/20170827001702_4683.webp) 从一开始这些校验就存在于早期的样式共享缓存的实现中。 但是也有很多其它小案例,样式可能匹配不上。 例如,如果 CSS 规则使用 :first-child 选择器,两个段落可能不匹配,即使以上校验表明它们匹配。 在 WebKit 和 Blink 里,样式共享缓存在这种情况下停止,不再使用缓存。 随着越来越多网站使用这些现代的选择器,这一优化变得用途越来越小,所以 Blink 团队最近把它移除了。 但事实上还有一种方法,让样式共享缓存能跟上这些变化。 Quantum CSS 先汇总这些特别的选择器,检查它们是否适用于 DOM 节点。 然后将答案以 1 和 0 的形式存储。 如果两个元素具有相同的 1 和 0,就知道它们绝对匹配。 ![](/uploads/article/2017/08/27/20170827001847_5026.webp) 如果 DOM 节点能共享已经计算过的样式,就可以跳过几乎所有的工作。 因为页面经常有很多 DOM 具有相同样式,样式共享缓存可以节省内存,并真的能加快速度。 ### 结论 这是从 Servo 到 Firefox 的第一个重大技术转移。 关于如何把 Rust 写出的现代化、高性能代码引进 Firefox 主干,一路上我们学到很多。
联系我们

邮箱 626512443@qq.com
电话 18611320371(微信)
QQ群 235681453

Copyright © 2015-2022

备案号:京ICP备15003423号-3