about:home 启动缓存

默认情况下,用户的浏览器会话以一个窗口和一个指向 about:home 的标签页开始。这意味着确保 about:home 尽可能快地加载以提供快速的整体启动体验非常重要。

about:home(在功能上与 about:newtab 相同)是通过在父进程中计算适当的状态对象并将其传递到内容进程中的 React 库以渲染最终的交互式页面来动态生成的。这在启动序列中存在问题,因为计算初始状态在计算上可能代价高昂,并且需要多次读取磁盘。

about:home 启动缓存旨在解决此开销。它的工作原理是假设在浏览器会话之间,about:home *通常* 不需要更改。

about:home 启动缓存机制的组件

缓存机制有三个主要组件

HTTP 缓存

HTTP 缓存通常用于缓存通过网络检索的网页,但似乎也适合存储 about:home 缓存。

在浏览网页时,网络栈通常会查询 HTTP 缓存。但是,在访问 chrome://resource:// URL 时,通常不会查询 HTTP 缓存,因此我们必须手动为 about:home 的情况手动执行此操作。这意味着为 about:home 提供填充和读取 HTTP 缓存的特殊功能。为了避免潜在的安全问题,这要求我们将 about:home / about:newtab 隔离到它们自己的特殊内容进程中。“特权 about 内容进程”为此目的而存在,也用于 about:loginsabout:certificate

HTTP 缓存位于父进程中,因此任何读写操作都需要在父进程中启动。幸运的是,HTTP 缓存使用 nsIOutputStream 接受数据并使用 nsIInputStream 提供数据。我们可以通过消息管理器发送 nsIInputStream,并将 nsIInputStream 转换为 nsIOutputStream,因此我们拥有所有必要的工具来有效地与“特权 about 内容进程”通信以保存和检索页面数据。

HTTP 缓存的官方文档可以在此处找到

AboutHomeStartupCache

此单例组件位于 BrowserGlue 内部,以避免在启动期间在父进程中从 omni.ja 文件加载另一个 JSM。

AboutHomeStartupCache 负责向“特权 about 内容进程”提供它需要呈现初始 about:home 文档的 nsIInputStream。它还负责使用“特权 about 内容进程”发送的更新版本填充 about:home 缓存。

由于访问 HTTP 缓存是异步的,因此存在竞争条件的可能性,缓存可以在请求初始 about:home 之前或之后被访问并可用。为了适应这两种情况,AboutHomeStartupCache 构造 nsIPipe 实例,并在启动一个实例后立即将其发送到“特权 about 内容进程”。

如果 HTTP 缓存条目在进程启动时已可用,并且缓存数据可用,我们将缓存连接到 nsIPipe 以将数据流式传输到“特权 about 内容进程”。

如果 HTTP 缓存尚不可用,我们将保存对这些 nsIPipe 实例的引用,并等待直到缓存条目可用。只有在那时,我们才会将缓存条目连接到 nsIPipe 实例以将数据发送到“特权 about 内容进程”。

AboutNewTabService

AboutNewTabService 用于父进程和内容进程中的 AboutRedirector,以确定如何处理加载 about:homeabout:newtab 的尝试。

AboutNewTabService 有不同的版本 - 一个用于父进程(BaseAboutNewTabService),一个用于内容进程(AboutNewTabChildService,它继承自 BaseAboutNewTabService)。

在“特权 about 内容进程”中运行的 AboutRedirector 知道将加载 about:home 的尝试重定向到 AboutNewTabChildServiceaboutHomeCacheChannel 方法。然后,此方法负责选择是返回缓存文档的 nsIChannel 还是返回 about:home 的动态生成版本。

AboutHomeStartupCacheChild

此单例组件位于“特权 about 内容进程”内部,并在从父进程接收到包含将用于从缓存潜在加载的 nsIInputStream 的消息后立即初始化。

当“特权 about 内容进程”中的 AboutRedirector 注意到已发出对 about:home 的请求时,它会要求 nsIAboutNewTabService 为该文档返回一个新的 nsIChannelAboutNewTabChildService 然后检查 AboutHomeStartupCacheChild 是否可以返回任何缓存内容的 nsIChannel

如果此时父进程尚未流式传输任何内容,我们将回退到加载动态 about:home 文档。如果缓存尚不存在,或者我们加载速度太慢而无法从磁盘获取,则可能会发生这种情况。随后加载 about:home 的尝试将绕过缓存并改为加载动态文档。即使特权 about 内容进程崩溃并创建了一个新进程,情况也是如此。

AboutHomeStartupCacheChild 还将负责定期生成缓存。定期,AboutNewTabService 将从父进程发送 about:home 的最新状态,然后 AboutHomeStartupCacheChild 将在 ChromeWorker 内使用 ReactDOMServer 生成文档标记。生成后,“特权 about 内容进程”将发送初始页面状态的标记和脚本的 nsIInputStream 实例。BrowserGlue 内部的 AboutHomeStartupCache 单例负责接收这些 nsIInputStream 并将其持久化到 HTTP 缓存中,以便下次启动时使用。

什么是缓存?

缓存了两样东西

  1. about:home 的原始 HTML 标记。

  2. 一小段 JavaScript 代码,通过 React 库“水化”标记,使页面在绘制后变得交互式。

由于 about:home 的 CSP 不允许内联脚本,因此无法将缓存的 JavaScript 代码直接作为内联脚本放入 HTML 标记中。相反,我们从 about:home?jscache 加载一个脚本。这将通过与从缓存中检索 HTML 文档相同的机制进行,但会下载缓存的脚本。

如果 HTML 标记被缓存,那么我们假设脚本也被缓存。我们不能缓存一个而不缓存另一个。如果只有一个缓存存在,或者在请求 about:home 文档时只有一个缓存被发送到“特权的 about 内容进程”,那么我们将回退到加载动态的 about:home 文档。

刷新缓存

缓存会定期刷新,方法是让 ActivityStreamMessageChannel 在它向预加载的 about:newtab 发送任何消息时告诉 AboutHomeStartupCache。一般来说,此类消息是一个很好的提示,表明下一个 about:newtab 的某些视觉内容已更新,并且可能应该刷新缓存。

AboutHomeStartupCache 会对有关此类消息的通知进行去抖动,因为这些通知往往会集中爆发。

使缓存失效

随着版本的更新,about:home 的组成或布局可能会发生变化。发生这种情况时,可能需要使用户可能存在的任何预先存在的缓存失效,以避免他们在启动时看到过时的 about:home

为此,我们在缓存条目上设置一个版本号,并确保版本号在启动时符合我们的预期。如果版本号与我们的预期不符,则缓存将被丢弃,并且 about:home 文档将被动态渲染。

版本号当前设置为应用程序的构建 ID。这意味着当应用程序更新时,在应用浏览器更新后的第一次重启时,缓存将失效。

处理错误

about:home 通常是用户启动浏览器时看到的第一件事。它必须快速且正确地运行至关重要。如果在检索或保存到缓存时发生任何错误,我们应该回退到动态生成文档。

例如,理论上浏览器在保存到缓存的过程中崩溃是可能的。在这种情况下,我们可能保存了一个部分文档或一个部分脚本——这两种情况都是不可接受的。

值得庆幸的是,HTTP 缓存的设计考虑了弹性,因此部分写入的条目会自动丢弃,这使我们能够回退到动态页面生成模式。

作为该弹性的额外冗余,我们还确保每次填充缓存时都创建一个新的 nsICacheEntry,并将版本元数据作为最后一步写入。由于版本元数据是最后写入的,因此我们知道,如果我们在尝试加载缓存时发现它丢失,则表示页面和脚本的写入未完成,并且我们应该回退到动态渲染页面。