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