当前位置: 首页 > Web前端 > CSS

图解Firefox的新CSS引擎

时间:2023-03-30 21:57:03 CSS

原文:InsideaSuperFastCSSEngine:QuantumCSS(AkaStylo),LinClark注:原文发表于2017年8月,本文翻译于2018年4月,所以文与时间相关的部分内容有所调整,但不影响核心内容。新的CSS引擎-Stylo您可能听说过ProjectQuantum,这是对Firefox浏览器内部结构的重大重写,以提高性能。在这次重写中,我们使用了来自并行浏览器引擎Servo的新技术。这里要介绍的是我们对浏览器引擎的重大改进。该项目的开发过程就像是在飞行中更换发动机。对于浏览器中的每个组件,我们都一一进行更改。这样,我们就可以在组件准备好后第一时间看到Firefox的最新效果。Servo的主要组件包括一个全新的CSS引擎,称为QuantumCSS(QuantumCSS,也称为Stylo)。这个新引擎集成了四种不同浏览器的最新创新,创建了一个全新的超级CSS引擎。它充分利用现代硬件的多核特性,将所有工作转化为并行操作。这使它快了2~4倍,甚至快了18倍。在并行化的基础上,它还结合了其他浏览器现有的最先进的优化技术。所以,即使没有并行化,它仍然是一个快速的CSS引擎。说到这里,我们不禁要问,这个CSS引擎到底是做什么的呢?要回答这个问题,您首先需要了解什么是CSS引擎以及它如何与浏览器的其他组件一起工作。然后,让我们看看Stylo是如何变得更快的。CSS引擎原理CSS引擎是浏览器渲染引擎(RenderingEngine)的重要组成部分。渲染引擎的工作是将网页的HTML和CSS文件转换成像素显示在屏幕上。每个浏览器都有一个渲染引擎。Chrome的叫Blink,Edge的叫EdgeHTML,Safari的叫WebKit,Firefox的叫Gecko。基本过程将一个文件变成像素,所有的渲染引擎基本上都做同样的工作,如下所示:1.将文件解析成浏览器可以理解的对象,包括DOM。从这个角度来看,DOM掌握了整个页面结构。它知道每个元素之间的父子关系,但不知道这些元素长什么样。2.弄清楚每个元素应该是什么样子。对于每个DOM节点,CSS引擎首先确定应该对其应用什么CSS规则。然后,计算每个CSS属性的值。3.计算每个节点的大小及其在屏幕上的位置。为每个要显示在屏幕上的内容创建盒子模型。这些盒子模型不仅仅用来表示DOM节点,它们还用来表示DOM节点的内部内容,比如一行文本。4.画出不同的盒子模型。这可能发生在多层上。就像以前用半透明纸的手绘动画一样,每一层都是一张单独的纸。这样我们就可以只改变当前图层的内容,而不会影响其他图层的内容。5.获取绘制的图层,应用任何仅合成器的属性(例如变换),并将它们合成为一个图像。这就像给这些叠加的图层拍照,然后将其渲染在屏幕上。从上面的过程可以看出,CSS引擎在开始计算样式的时候,得到了两个东西:DOM树样式规则列表引擎会一一遍历所有的DOM节点,计算出它们的样式。在此过程中,它评估DOM节点上的每个CSS属性,包括未在样式表中声明的属性。这个过程就像一个人从头到尾填写一张表格。CSS引擎需要为每个DOM节点填写一个表单。此外,表格中的每个空白都需要填写一个值。为了填写这个表格,CSS引擎需要做两件事:计算每个节点应该应用什么样的样式规则,即选择器匹配(SelectorMatching),以及根据parent计算缺失的属性值节点或默认值,即样式级联(Cascading)选择器匹配在这一步,CSS引擎会将所有匹配DOM节点的样式规则添加到一个列表中。因为可能有多个样式规则匹配,所以同一个CSS属性可能有多个声明。另外,浏览器本身也提供了一些默认的样式规则,即用户代理样式表。那么CSS引擎如何确定应该使用哪个值呢?这时候就需要特异性规则(SpecificityRules)来帮忙了。CSS引擎创建一个电子表格,然后根据不同的列对其进行排序。最终,具有最高特异性的规则获胜。所以,基于这个表,CSS引擎可以填充那些它可以填充的属性值。对于无法通过这种方式计算的值,它使用样式级联。StyleCascadeStyleCascade使编写和维护CSS变得更加容易。因为样式级联,你可以直接知道

  • 在设置的color属性后会使用你设置的颜色值(除非被覆盖)。为了找到级联样式属性,CSS引擎会查看表格中的空白区域。如果属性默认为继承值,CSS引擎将查找DOM树以查看其祖先元素是否已设置该值。如果所有祖先元素都没有设置值,或者属性未被继承,则将使用默认值。至此,一个DOM节点的所有样式属性都已经计算完毕。样式结构共享其实上面说的样式表与实际情况并不完全一致。CSS有大量的样式属性,多达数百个。如果CSS引擎为每个DOM节点属性保留样式值的副本,内存将很快耗尽。相反,CSS引擎通常使用StyleStructSharing。它将通常一起使用的样式值存储在称为样式结构的单独对象中。然后,不是将所有样式值重新存储在同一个对象上,而是计算出的样式对象实际上只保存指向该对象的指针。对于每一类风格,实际存储的是一个指向风格结构体的指针。这种共享方法既节省内存又节省时间。这样,具有相似样式的节点(例如兄弟节点)只需要存储指向共享样式结构对象的指针。此外,由于继承了很多属性,祖先节点可以与所有后代节点共享相同的样式结构对象。优化改进上面说的就是优化之前的样式计算过程。这个过程涉及大量的计算工作。而且它不仅仅发生在第一页加载时。它会在用户与页面交互的过程中反复发生,将鼠标悬停在元素上或改变DOM结构都会触发样式重新计算(Restyle)。也就是说,CSS样式计算是需要优化的重点。在过去的20年里,浏览器一直在测试不同的策略来优化它。Stylo充分吸收了不同引擎的优化策略,然后将它们结合起来,打造出一个全新的超级引擎。我们来看看Stylo的实现细节。并排运行Servo是一个实验性浏览器,而Stylo是该项目的一部分。Servo希望并行化渲染页面所需的所有工作。并行化到底是什么意思?计算机就像大脑。其中之一专门用于逻辑思维,称为算术逻辑单元(ALU)。在ALU附近,有一些称为寄存器的短期记忆存储单元。ALU和寄存器都放在CPU内部。当然也有长期记忆的存储单元,称为随机存取存储器(RAM)。早期使用这种CPU的计算机一次只能做一件事。但是,近十年来,CPU已经进化到同时拥有多个ALU和寄存器组,拥有多个核心。这意味着CPU可以同时做多件事。Stylo利用计算机的这一特性,将不同DOM节点的样式计算过程分散到不同的计算核心。这看起来是一件很简单的事情,只需要将DOM树的不同分支分开,交给不同的核心即可。但它实际上比你想象的更难,原因之一是DOM树通常是不均匀的。这导致一些核心比其他核心做更多的工作。为了更均匀地分配工作,Stylo采用了一种称为工作窃取的技术。在处理一个DOM节点时,运行代码将其子节点划分为一个或多个WorkUnit。这些工作单元被添加到一个队列中。当一个核心完成它的工作队列时,它会查看其他队列中的工作单元并带走它们去做。这样,我们就可以更均匀地分配工作,而无需花时间遍历DOM树或提前弄清楚如何平均分配工作。在大多数浏览器中,这种并行化是很难做到的。我们都知道并行化是一个棘手的难题,而CSS引擎也很复杂。同时,CSS引擎仍然处于其他两个最复杂部分的中间地带:DOM和布局。因此,非常容易引入错误,并行化也会造成非常难以追踪的错误,称为数据竞争(DataRaces)。这类错误我在另一篇文章中有详细介绍,有兴趣的可以参考。如果您接受数百名工程师的代码贡献,您如何进行并行编程而不担心错误?这就是Rust的用武之地。在Rust中,您可以通过静态检查避免数据竞争。也就是说,您可以直接在代码中避免此类难以调试的错误。编译器不会让你的代码出现这样的问题。使用Rust,CSS样式计算成为所谓的完美并行问题,因为您基本上不做任何事情来并行化它们。这意味着我们的优化可以实现线性增长。如果您的机器有4个内核,那么您的性能将提高近4倍。规则树对于每一个DOM节点,CSS引擎需要遍历所有的样式规则来完成选择器匹配。但是对于大多数节点来说,这个匹配规则并不会经常变化。例如,如果用户将鼠标悬停在父元素上,则该元素的匹配样式规则可能会发生变化。我们还需要重新计算其后代元素的样式以重新处理那些继承的属性。当然,匹配这些后代元素的规则也可能是不变的。如果我们可以记录哪些规则匹配这些后代元素,这样我们就不需要为它们重新匹配选择器,那就太好了。这就是我们在Firefox中借鉴了上一代CSS引擎的规则树(RuleTree)的原理。CSS引擎完成匹配选择器的过程,然后按特异性排列它们,创建一个规则链表。链表将被添加到规则树中。CSS引擎会尝试将规则树的分支数保持在最低限度。为此,它尝试尽可能多地重用现有规则分支??。如果链表中的选择器与现有分支相同,则它将沿着相同的路径向下移动。但是,它最终可能会转到具有不同下一条规则的节点,然后引擎才会添加新分支。DOM节点获得指向规则结束节点的指针(在本例中为div#warning规则)。而且,这是具有最高特异性的规则。在样式重新计算期间,CSS引擎会快速检查父元素的更改是否会影响匹配子元素的规则。如果不受影响,那么对于任何一个后代节点,引擎只需要按照后代节点保存的规则指针去寻找对应的规则分支即可。在规则树中,向上遍历树至根节点即可得到所有匹配的样式规则。即CSS引擎完全跳过了选择器匹配和具体的排列过程。通过这种方式,我们减少了样式重新计算过程的计算量。即便如此,在样式初始化的时候还是会消耗大量的计算。如果你有10,000个节点,你仍然需要进行10,000次选择器匹配。不过别担心,我们还有另一种优化方法。StyleSharedCache对于一个有几万个节点的页面,很多节点都会匹配同一个样式规则。例如,对于一个非常长的wiki页面,主要内容区域中的段落都应该应用相同的样式规则,因此具有相同的计算样式。如果这里没有优化,CSS引擎必须对每个段落进行选择器匹配和样式计算。但是如果有办法证明这些不同的段落使用了相同的样式,那么引擎只需要做一次计算,然后所有其他的段落节点都指向相同的计算样式。这就是我们所说的样式共享缓存,灵感来自Safari和Chrome。引擎处理完一个节点后,会将计算出的样式放入缓存中。然后,在开始计算下一个节点的样式之前,引擎会进行一些检查以查看是否可以使用缓存的样式。这些检查包括:两个节点是否具有相同的id、class等?如果是,那么它们可以匹配.对于任何不基于选择器的样式,例如内联样式,节点是否具有相同的样式值?如果是,则从父节点继承的属性不会被覆盖,或者以相同的方式被覆盖。节点的父节点是否指向相同的计算样式对象?如果是,那么继承的样式值是相同的。自从提出样式共享缓存以来,就已经应用了这些检查。然而,随着CSS的发展,还有许多其他小场景可以使检查样式共享缓存的方式失效。例如,如果CSS规则使用:first-child选择器,两个段落元素可能会导致样式不一致,即使上面的检查认为它们是相同的。在WebKit和Blink中,样式共享缓存忽略了这些场景,不使用缓存。随着越来越多的网站使用现代选择器,样式共享缓存的优化效果越来越差,因此Blink团队最终将其移除。然而,事实证明,样式共享缓存有办法跟上这些演变。在Stylo中,我们跟踪所有这些现代选择器并检查它们是否适用于DOM节点。然后,我们将检查结果存储为0和1。如果两个元素具有相同的0和1,那么我们可以确定它们匹配。如果一个DOM节点可以使用已经计算好的样式缓存,那么引擎可以直接跳过很多计算过程。由于页面中往往存在大量具有相同样式规则的DOM节点,样式共享缓存不仅可以节省内存,还可以加快计算过程。结论Stylo是第一个从Servo迁移到Firefox的大型技术。一路上,我们学到了很多关于如何将用Rust编写现代高性能代码集成到Firefox核心中的知识。事不宜迟,下载Firefox并体验极速!