提升了 Chromium 无障碍功能的性能

本文作者是 Chromium 贡献者 Ahmed Elwasefi,他分享了自己是如何通过 Google 夏季编程活动成为贡献者的,以及他发现并修复了哪些无障碍功能性能问题。

在开罗德国大学计算机工程专业即将进入最后一年时,我决定探索为开源项目做出贡献的机会。我开始探索 Chromium 的适合新手的问题列表,并发现自己特别喜欢无障碍功能。在寻求指导的过程中,我遇到了 Aaron Leventhal。他的专业知识和乐于助人的态度激励我与他合作完成一个项目。这次合作成为了我的 Google 夏季编程活动经历,我被邀请加入 Chromium 无障碍团队。

成功完成 Google 夏季编程活动后,出于提升性能的目的,我继续解决一个未解决的滚动问题。得益于 Google 的 OpenCollective 计划提供的两项资助,我不仅能够继续推进此项目,还能承担其他任务,专注于简化代码以提升性能。

在本博文中,我将分享自己在过去一年半内与 Chromium 共事的经历,详细介绍我们在技术方面(尤其是在性能方面)所做的改进。

无障碍功能代码对 Chrome 性能有何影响

Chrome 的无障碍代码可帮助屏幕阅读器等辅助技术访问网络。不过,启用该功能可能会影响加载时间、性能和电池续航时间。因此,如果不需要,此代码会保持不活动状态,以免降低性能。大约 5-10% 的用户启用了无障碍功能代码,这通常是因为密码管理工具和杀毒软件等工具使用了平台无障碍功能 API。这些工具依赖于这些 API 来与网页内容互动和进行修改,例如为密码管理工具和表单填充工具定位密码字段。

核心指标的总体下降幅度尚不明确,但一项名为“自动停用无障碍功能”(在无障碍功能未使用时将其关闭)的近期实验表明,下降幅度相当高。之所以出现此问题,是因为 Chrome 无障碍功能代码库的两个主要领域(即渲染程序和浏览器)中存在大量计算和通信。渲染程序会收集有关 Web 内容和内容更改的信息,并计算节点树的无障碍功能属性。然后,系统会序列化所有脏节点,并通过管道将其发送到浏览器进程的主界面线程。此线程会接收并将此信息反序列化为相同的节点树,然后最终将其转换为适合第三方辅助技术(例如屏幕阅读器)的形式。

改进了 Chromium 无障碍功能

以下项目是在 Google 夏季编程活动期间和之后完成的,由 Google OpenCollective 计划资助。

缓存改进

在 Chrome 中,有一个名为无障碍树的特殊数据结构,用于镜像 DOM 树。它用于帮助辅助技术访问 Web 内容。有时,当设备需要此树中的信息时,该树可能还未准备就绪,因此浏览器必须将这些请求安排到稍后处理。

以前,此调度是使用一种称为闭包的方法处理的,该方法涉及将回调放入队列。由于闭包的处理方式,这种方法会增加额外的工作量。

为了改进这一点,我们改用了一个使用枚举的系统。每个任务都会分配一个特定的枚举值,并且在无障碍树准备就绪后,系统会调用适用于该任务的正确方法。这一更改使代码更易于理解,并将性能提高了 20% 以上。

运行时性能测试图表。
一张图表,显示了多项性能测试的运行时间,其中所有测试的运行时间都明显下降了约 20%。

查找和解决滚动性能问题

接下来,我探索了关闭边界框序列化后性能如何提升。边界框是网页上元素的位置和大小,包括宽度、高度以及相对于父元素的位置等详细信息。

为了测试这一点,我们暂时移除了处理边界框的代码,并运行了性能测试以了解影响。其中一个测试(focus-links.html)的效果提升幅度高达约 1618%。这一发现成为了后续工作的基石。

调查运行缓慢的测试

我开始调查使用边界框时该特定测试运行缓慢的原因。该测试只会将焦点依次放在多个链接上。因此,主要问题必须是聚焦于元素,或者是焦点操作发生的滚动。为了测试这一点,我在性能测试中的 focus() 调用中添加了 {preventScroll: true},以停止滚动。

停用滚动后,在边界框计算处于活动状态时,测试时间缩短至 1.2 毫秒。这表明滚动是真正的问题。

停用滚动功能后的测试结果。
停用滚动或移除边界框序列化后,焦点链接测试运行时从 20 毫秒降至 1.1 毫秒。

我创建了一个名为 scroll-in-page.html 的新测试来重现 focus-links 测试,但它使用 scrollIntoView() 滚动浏览元素,而不是使用焦点。我测试了平滑滚动和即时滚动,以及在计算边界框和不计算边界框的情况下进行滚动。

新测试的测试结果。
在即时滚动中处理滚动所需的时间为 65 毫秒,而在流畅滚动中则为 123 毫秒。

结果表明,使用即时滚动和边界框时,该过程大约需要 66 毫秒。流畅滚动速度甚至更慢,大约为 124 毫秒。当我们关闭边界框时,系统根本没有花费任何时间,因为没有触发任何事件。

我们知道这个支持请求,但为什么会出现这种情况?

我们现在已经了解到,滚动是无障碍功能序列化速度缓慢的主要原因,但我们仍需要找出原因。为了分析这一点,我们使用了名为 perfpprof 的两个工具来细分浏览器进程中完成的工作。这些工具通常用于 C++ 中的性能分析。下图展示了有趣部分的摘要。

性能剖析滚动测试的图表。
通过滚动测试进行性能分析生成的图表。显示大部分时间都花在调用名为 Unserialize 的函数以及名为 IsChildOfLeaf 的函数上。

经过调查,我们发现问题不是反序列化代码本身,而是对其的调用频率。为了了解这一点,我们需要了解无障碍功能更新在 Chromium 中的运作方式。更新不会单独发送;而是有一个名为 AXObjectCache 的中央位置存储所有媒体资源。当节点发生变化时,各种方法会通知缓存将该节点标记为,以便日后序列化。然后,脏记事的所有属性(包括未更改的属性)都会序列化并发送到浏览器。虽然这种设计通过使用单个更新路径简化了代码并降低了复杂性,但当出现快速的“标记为脏”事件(例如滚动事件)时,速度会变慢。唯一会发生变化的是 scrollXscrollY 值;不过,我们会每次都将其余属性与它们一起序列化。更新速度达到每秒 20 次以上!

边界框序列化通过使用仅发送边界框详细信息的更快序列化路径来解决此问题,从而实现快速更新,而不会影响其他属性。此方法可高效处理边界框更改。

滚动修复

解决方案很明确:在边界框序列化中添加当前滚动偏移量。这样可确保滚动更新通过快速路径进行处理,从而提高性能,而不会出现不必要的延迟。通过将滚动偏移量与边界框数据打包,我们优化了该流程,以实现更流畅、更高效的更新,为启用了无障碍功能的用户打造了更流畅的体验。在滚动测试中,实施修复后,性能最高可提高 825%

代码简化

在此期间,我专注于提高代码质量,并参与了一个名为 Onion Soup 的项目,该项目通过减少或移除不必要地在各个层之间扩散的代码来简化代码。

第一个项目旨在简化无障碍功能数据从渲染程序序列化到浏览器的方式。以前,数据必须先通过额外的层才能到达目的地,这增加了不必要的复杂性。我们简化了此流程,允许直接发送数据,从而省去了中间人。

此外,我们还发现并移除了一些导致系统中执行不必要工作已过时のイベント,例如在布局完成时触发的事件。我们已将这些方法替换为更高效的解决方案。

我们还进行了其他一些小改进。很遗憾,我们没有记录这些方面的性能改进,但我们很高兴地告诉您,代码比以前更加清晰易懂,并且自文档化程度更高。这有助于为日后提升广告效果奠定基础。您可以在我的 gerrit 个人资料中查看实际更改。

总结

与 Chromium 无障碍团队合作是一次富有成效的经历。 通过解决各种挑战(从优化滚动性能到简化代码库),我对此类大型项目的开发有了更深入的了解,并学习了重要的性能分析工具。此外,我还了解到,无障碍功能对于打造包容所有人的网络至关重要。我们所做的改进不仅改善了依赖辅助技术的用户的体验,还提升了浏览器的整体性能和效率。

效果非常显著。例如,改为使用枚举来调度任务后,性能提高了 20% 以上。此外,滚动修复导致滚动测试时间最多缩短了 825%。代码简化更改不仅使代码更清晰、更易于维护,还为未来的增强功能奠定了基础。

我要感谢 Stefan Zager、Chris Harrelson 和 Mason Freed 在整个一年中的支持和指导,尤其是 Aaron Leventhal,如果没有他,我不可能获得这次机会。我还想感谢 Tab Atkins-Bittner 和 GSoC 团队的支持。

对于希望为有意义的项目做出贡献并提升技能的人,我强烈建议参与 Chromium 项目。这是一种非常棒的学习方式,而 Google 夏季编程活动等计划是您开启编程之旅的绝佳起点。