WebKit 的 CSS JIT 编译器概述
说到性能,JavaScript 通常会成为头条新闻。但仔细观察,网页并非只有 JavaScript,所有其他部分的性能也对用户体验有着巨大的影响。只关注 JavaScript 只提供了对网页性能这一复杂拼图的一个视角。
让 CSS 更快、更具可伸缩性是 WebKit 项目的一个研究领域。DOM 和 CSS 并不总是很好地伸缩,遗憾的是,在复杂的动画中看到丢帧仍然很常见。
我们在 JavaScript 中用来加速的技术是使用即时编译器(JIT)消除慢速路径。CSS 是 JIT 编译的绝佳候选者,自去年年底以来,WebKit 会动态编译某些 CSS 选择器。
这篇博文介绍了 CSS 选择器 JIT,它的优缺点,以及您如何帮助我们改进 CSS。
如何以及为何编译 CSS?
CSS 是告诉引擎如何将 DOM 树呈现在屏幕上的语言。每个网页引擎都使用复杂的机制,遍历 DOM 中的所有内容,并应用 CSS 定义的规则。
DOM 树和样式规则之间没有直接链接。对于每个元素,引擎需要收集适用于它的一系列规则。CSS 和 DOM 树通过 CSS 选择器连接起来。
每个选择器以紧凑的格式描述一个元素要获得特定样式所需的属性。引擎必须找出哪些元素具有所需的属性,这通过在元素上测试选择器来完成。正如您所想,对于最微不足道的 DOM 树之外的任何情况,这都是一项复杂的任务(幸运的是,WebKit 在这方面有很多优化)。
那么我们是如何让它更快呢?我们选择的一个简单解决方案是让在元素上测试选择器变得非常快。
选择器匹配过去的工作方式是通过一个软件机器,即 SelectorChecker 实例,它接受两个输入:一个选择器和一个输入元素。给定这些输入,SelectorChecker 遍历选择器的每个部分,并尝试在以输入元素结尾的树中找到所需的属性。
下图展示了选择器测试过去工作方式的简化版本
SelectorChecker 的问题在于它需要完全通用。我们有一个复杂的选择器解释器,能够处理任何选择器的任何困难情况组合。不幸的是,庞大的通用机器并不快。
使用 CSS JIT 时,匹配选择器的任务被分成两部分:先编译,后测试。JIT 编译器接受选择器,在编译时执行所有复杂的计算,并生成对应于输入选择器的一个微小二进制块:一个已编译的选择器。当需要查找匹配该选择器的元素时,WebKit 只需调用已编译的选择器即可。
以下动画展示了与上面相同的过程,但使用了 CSS 选择器 JIT
显然,与 CSS 选择器相关的所有复杂性仍然存在。选择器 JIT 编译器是一个庞大的通用机器,就像 SelectorChecker 一样。变化之处在于,大部分复杂性已转移到编译时,并且只发生一次。运行时生成的二进制文件只与输入选择器一样复杂。
简单之美
尽管有人可能认为使用 JIT 总是能加快执行速度,但这是一种谬论。事实是,增加一个编译器最初会使所有事情变慢,然后编译器通过创建非常快的机器代码来弥补这一点。只有当编译器和已编译代码的总执行时间小于编译器本身的执行时间时,整个过程才算有益。
当工作负载很小时,编译器花费的时间大于增益。例如,假设我们有一个 JIT 编译器,比 SelectorChecker 慢 4 倍,但已编译代码比 SelectorChecker 快 4 倍。这是单次执行的时间图
在这种时间安排下,我们可以在旧的 C++ 选择器检查器上运行 5 次完整的查询,仍然比 JIT 快。
当 JIT 编译器足够快且工作负载足够大时,已编译版本就会胜出
这个限制也是长时间运行的基准测试可能具有误导性的原因之一,它们可以隐藏慢速编译器。JIT 编译器有助于长期运行程序获得出色的吞吐量,但实际的网页并非如此。编译引入的延迟也可能对动画造成灾难。
这是否意味着我们自己给自己挖了个坑,做出了一个只在基准测试中快的东西?并非如此,我们也解决了这个问题。
有几种方法可以减轻 JIT 编译器引入的延迟。JavaScriptCore 使用多种高级子系统来实现这一目标。到目前为止,选择器 JIT 可以用一个简单的解决方案解决问题:让编译器极快。
这个编译器的速度有两个关键部分。
- 首先,编译器非常简单。进行优化可能需要大量时间,因此我们决定只进行很少的优化。生成的二进制文件并非完美,但生成速度很快。
- 第二个诀窍是使用非常快速的二进制生成。为此,编译器构建在 JavaScriptCore 的基础设施之上。JavaScriptCore 拥有能够极快生成二进制文件的工具,我们直接在 WebCore 中使用它。
在 JIT 的最新版本中,编译阶段与 SelectorChecker 的单次执行时间处于一个数量级之内。考虑到即使是很小的页面也有几十个选择器和数百个元素,很容易就能弥补编译器花费的时间。
它有多快?
为了提供数量级的概念,我为这篇博客准备了一个小型微基准测试。它测试了各种用例,包括过去在 WebKit 上很慢的情况。
在我的 Retina Macbook Pro 上,基准测试在去年 12 月的 WebKit 上运行大约需要 1100 毫秒,而在今天的 WebKit nightly 版本上运行不到 500 毫秒。对于常见选择器,我们通常期望获得 2 倍的性能提升。
显然,速度提升很大程度上取决于页面。如果旧的 WebKit 触及了某个慢速路径,提升有时会大得多;对于琐碎或未编译的选择器,提升可能会小一些。我预计未来会有很多变化,我希望我们能得到更多反馈,以帮助塑造 CSS 性能的未来。
querySelector 呢?
querySelector()
和 querySelectorAll()
函数目前与样式解析共享大部分基础设施。在许多情况下,这两个函数也将受益于 CSS JIT 编译器。
通常,querySelector API 的使用方式与样式解析有很大不同。因此,我们对其进行单独优化,以便每个子系统都能在其特定用例中达到最快。这带来的一个副作用是,querySelector 的性能表现并不总是能很好地反映样式解析的选择器性能,反之亦然。
您如何提供帮助?
目前正在进行工作以支持 SelectorChecker 可以处理的所有情况。目前,一些伪类型不受 JIT 编译器支持,WebKit 会回退到旧代码。缺少的部件正在一点一点地添加。
有很多机会可以帮助提高 CSS 的速度。现有的 CSS 基准测试非常有限,没有像 JSBench 那样针对 CSS 的工具。因此,我们从实际网站上遇到的性能问题中获得的输入非常有价值。
如果您是网页开发者或 WebKit 爱好者,请尝试使用WebKit Nightly访问您最喜欢的网站。如果您遇到 CSS 性能问题,请在WebKit 的错误跟踪器上提交 Bug。到目前为止,提交的关于 CSS JIT 的每一个 Bug 都非常有帮助。
最后,如果您对实现细节感兴趣,所有代码都是开源的,可在 webkit.org 上获取。欢迎您帮助改进网络。
您可以在 Twitter 上向我发送问题,我的账号是@awfulben。对于更深入的讨论,您可以向webkit-help发送电子邮件(或提交 Bug 报告)。