我是 Blink 布局团队的工程主管 Ian Kilpatrick,Koji Ishii 是我的同事。在加入 Blink 团队之前,我曾是一名前端工程师(在 Google 设立“前端工程师”这一职位之前),负责在 Google 文档、云端硬盘和 Gmail 中构建功能。在担任这个职务大约五年后,我孤注一掷地投身到 Blink 团队,在工作中有效学习 C++ 知识,并尝试提升超复杂的 Blink 代码库。 即便是今天,我也只能理解其中的一小部分。 感谢您在此期间耐心等待。 令我感到欣慰的是,在我之前,许多“正在转型为前端工程师”的工程师都转型为“浏览器工程师”。
我在 Blink 团队任职期间,曾以往的经验给予了我个人指导。 作为一名前端工程师,我经常遇到浏览器不一致性、性能问题、渲染 bug 和缺少功能的问题。LayoutNG 让我有机会帮助 Blink 的布局系统系统地解决这些问题,它代表了我们多年来众多工程师的不懈努力。
在本文中,我将介绍这样一项重大架构变更如何减少和缓解各种类型的 bug 和性能问题。
布局引擎架构的宏观视图
以前,Blink 的布局树被称为“可变树”。
布局树中的每个对象都包含输入信息(例如父级强加的可用大小、任何浮点对象的位置)和输出信息(例如对象的最终宽度和高度或其 x 和 y 位置)。
这些对象会在渲染之间保留。当样式发生变化时,我们会将该对象标记为脏,并将树中的所有父级标记为脏。运行渲染流水线的布局阶段后,我们会清理树、遍历所有脏对象,然后运行布局以使其处于清洁状态。
我们发现,这种架构导致了许多类别的问题,我们将在下文进行介绍。 但首先,让我们回过头来考虑一下布局的输入和输出是什么。
从概念上讲,对此树中的节点运行布局会采用“样式加 DOM”,以及来自父布局系统(网格、块或 flex)的任何父级约束条件,运行布局约束条件算法,并生成结果。
我们的新架构正式化了这种概念模型。 我们仍然保留布局树,但主要用它来保存布局的输入和输出。 对于输出,我们会生成一个名为 fragment 树的全新不可变对象。
我之前介绍了不可变 fragment 树,介绍了它是如何设计的,以便重复使用之前树的大部分内容来实现增量布局。
此外,我们还会存储生成该 fragment 的父约束条件对象。我们将其用作缓存键,下文会对此进行详细介绍。
我们还重写了内嵌(文本)布局算法,以匹配新的不可变架构。 它不仅会为内嵌布局生成不可变的扁平列表表示法,还具有段落级缓存以加快重新布局速度、按段落设置形状以跨元素和字词应用字体功能、使用 ICU 的新 Unicode 双向算法、大量正确性修复等。
布局 bug 的类型
从广义上讲,布局 bug 可分为四类,每类都有不同的根本原因。
正确性
当我们考虑渲染系统中的 bug 时,通常会考虑正确性,例如:“浏览器 A 具有 X 行为,而浏览器 B 具有 Y 行为”,或“浏览器 A 和 B 都已损坏”。以前,我们花了很多时间来解决这个问题,在此过程中,我们一直在与系统斗争。一种常见的失败模式是,针对一个 bug 应用非常有针对性的修复程序,但几周后发现,我们在系统的另一个(似乎不相关)部分造成了回归。
如之前的文章中所述,这表明系统非常脆弱。具体而言,就布局而言,我们的任何类之间都没有清晰的协定,这导致浏览器工程师依赖于他们不应该依赖的状态,或错误解释了系统其他部分的某个值。
例如,在某个时间点,我们在一年多的时间里发现了与 Flex 布局相关的约 10 个 bug 链。每次修复都会导致系统的某个部分正确性或性能问题,从而导致另一个 bug。
现在,LayoutNG 已明确定义布局系统中所有组件之间的协定,我们发现自己可以更放心地应用更改了。我们还受益于出色的 Web 平台测试 (WPT) 项目,该项目允许多方为一个通用的 Web 测试套件做出贡献。
现在,我们发现如果在稳定版渠道上发布了真正的回归问题,则 WPT 代码库中通常没有相关测试,并且回归问题也不是因误解组件协定而导致的。此外,根据我们的 bug 修复政策,我们始终会添加新的 WPT 测试,以确保任何浏览器都不会再次犯同样的错误。
失效中
如果您曾遇到过一个神秘的错误,即调整浏览器窗口的大小或切换 CSS 属性神奇地使该错误消失,那么您就遇到了失效不足的问题。 实际上,系统会将可变树的一部分视为干净,但由于父级约束条件发生了一些变化,因此它并未提供正确的输出。
在下面介绍的两次遍历(两次遍历布局树以确定最终布局状态)布局模式中,这种情况非常常见。以前,我们的代码如下所示:
if (/* some very complicated statement */) {
child->ForceLayout();
}
此类 bug 的修复方法通常如下:
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
此类问题的修复通常会导致严重的性能回归(请参阅下文中的过度失效),并且很难正确解决。
目前(如上所述),我们有一个不可变的父级约束条件对象,用于描述从父布局传递给子布局的所有输入。我们将其与生成的不可变 fragment 一起存储。因此,我们在一个集中位置对这两个输入进行差异化处理,以确定子项是否需要执行另一个布局传递。此差异比较逻辑很复杂,但井井有条。调试此类失效问题通常会导致手动检查两个输入,并确定输入中发生了哪些变化,以便确定是否需要进行另一次布局传递。
由于创建这些独立对象非常简单,因此修复此差异代码通常很简单,并且易于单元测试。
上述示例的差异代码如下所示:
if (width.IsPercent()) {
if (old_constraints.WidthPercentageSize()
!= new_constraints.WidthPercentageSize())
return kNeedsLayout;
}
if (height.IsPercent()) {
if (old_constraints.HeightPercentageSize()
!= new_constraints.HeightPercentageSize())
return kNeedsLayout;
}
滞后
此类 bug 类似于失效不足。从本质上讲,在之前的系统中,确保布局具有幂等性非常困难,也就是说,使用相同的输入重新运行布局会产生相同的输出。
在下面的示例中,我们只是在两个值之间来回切换 CSS 属性。不过,这会导致“无限增长”的矩形。
使用之前的可变树时,引入这样的 bug 非常容易。 如果代码错误地在错误的时间或阶段读取对象的大小或位置(例如,我们没有“清除”之前的大小或位置),我们会立即添加一个细微的迟滞错误。 由于大多数测试都侧重于单个布局和渲染,因此这些 bug 通常不会出现在测试中。 更令人担心的是,我们知道需要一些这种滞后才能让某些布局模式正常运行。我们遇到过一些 bug,我们需要执行优化来移除一次布局遍历,但会引入一个“bug”,因为布局模式需要两次遍历才能获取正确的输出。
在 LayoutNG 中,由于我们有明确的输入和输出数据结构,并且不允许访问之前的状态,因此我们大大减少了布局系统中此类 bug。
过度失效和性能
这与“失效不足”类 bug 完全相反。通常,在修复无效系统错误的 bug 时,我们会导致性能下降。
我们经常不得不做出艰难的选择,以正确性为先,而非性能。在下一部分中,我们将深入探讨如何缓解此类性能问题。
双通道布局和性能悬崖上升
Flex 和网格布局代表了 Web 布局表现力的变化。 不过,这些算法与之前的块布局算法有着根本性的不同。
在几乎所有情况下,块布局只要求引擎对其所有子项执行一次布局。这对性能有很大帮助,但最终的效果不如 Web 开发者所希望的那样出色。
例如,您通常希望将所有子项的大小扩展到最大的子项的大小。为了支持此功能,父布局(flex 或 grid)将执行测量传递以确定每个子项的大小,然后执行布局传递以将所有子项拉伸到此大小。这种行为是 flex 布局和网格布局的默认行为。
从性能方面来看,这些两次传递的布局最初是可以接受的,因为用户通常不会对其进行深层嵌套。不过,随着更复杂的内容的出现,我们开始发现显著的性能问题。 如果您不缓存测量阶段的结果,布局树将在其测量状态和最终布局状态之间进行大量重复计算。
以前,我们会尝试向 Flex 和网格布局添加非常具体的缓存,以对抗此类性能断崖。这种方法可行(并且我们在 Flex 的使用方面取得了很大进展),但一直在与多次失效的失效错误进行斗争。
借助 LayoutNG,我们可以为布局的输入和输出创建明确的数据结构,此外,我们还构建了测量和布局传递的缓存。这会使复杂性恢复为 O(n),从而为 Web 开发者带来可预测的线性性能。如果布局正在执行三次布局,我们也会直接缓存该传递。这可能会为未来安全地引入更高级的布局模式提供机会,这是一个很好的例子,说明 RenderingNG 如何从根本上解锁全面的可扩展性。在某些情况下,网格布局可能需要三次布局,但目前这种情况非常罕见。
我们发现,如果开发者遇到布局方面的性能问题,通常是由于指数布局时间 bug 造成的,而不是流水线布局阶段的原始吞吐量。 如果进行小幅增量更改(一个元素更改单个 CSS 属性)导致布局延迟 50-100 毫秒,则很可能是指数级布局 bug。
总结
布局是一个极其复杂的领域,我们并未涵盖各种有趣的细节,例如内嵌布局优化(整个内嵌和文本子系统的实际运作方式),甚至这里讨论的概念也只是皮毛而已,很多细节都没有提及。不过,我们希望已经证明,系统性地改进系统架构可以带来长期的巨大收益。
尽管如此,我们知道自己仍有大量工作要做。 我们知道存在一些问题(包括性能和正确性问题),正在努力解决这些问题,并对 CSS 即将推出的新布局功能感到兴奋。我们相信,LayoutNG 的架构可让您更轻松、安全地解决这些问题。
Una Kravets 制作的一张图片(你知道哪张图片!)。