异步选项卡切换器

在非常高的层次上,异步选项卡切换器负责告诉具有进程外(或“远程”)<xul:browser>的选项卡渲染并将其内容上传到合成器,然后更新 UI 以显示该内容作为选项卡切换。类似地,异步选项卡切换器负责告诉已切换离开的选项卡停止渲染其内容,以及合成器释放这些内容。

简要介绍图层和合成器

对于进程外选项卡,Gecko 的呈现部分在选项卡内容进程内计算选项卡的最终内容,然后将该信息上传到合成器。此上传的信息通常称为图层

合成器最终将这些图层作为像素呈现给用户。合成器可以保留多个图层集,而无需将其显示给用户,但这会消耗内存。不再需要的图层将被释放。

从现在开始,“选项卡内容”将被称为该选项卡的图层

renderLayers、hasLayers、docShellIsActive

<xul:browser>在其上公开了一些有用的属性,异步选项卡切换器使用这些属性

renderLayers

对于远程 <xul:browser>,将其从 false 设置为 true 表示请求内容进程渲染该 <xul:browser> 的图层并将其上传到合成器。将其从 true 设置为 false 表示请求内容进程停止渲染图层并让合成器释放图层。当此属性已经是 true 时将其设置为 true 或当其已经是 false 时将其设置为 false 是一个无操作。当此属性返回 true 时,这意味着已为该选项卡请求了图层,但不能保证合成器是否已收到图层。类似地,当此属性返回 false 时,这意味着已请求此浏览器停止渲染图层,但不能保证合成器是否已释放图层。

对于非远程 <xul:browser>renderLayersdocShellIsActive 的别名。

hasLayers

对于远程 <xul:browser>,此只读属性如果合成器具有该选项卡的图层,则返回 true,否则返回 false

对于非远程 <xul:browser>hasLayers 返回 docShellIsActive 的值。

docShellIsActive

对于远程 <xul:browser>,将 docShellIsActive 设置为 true 也会将 renderLayers 设置为 true,然后向内容进程发送消息以将其顶级 docShell 活动状态设置为 true。类似地,将 docShellIsActive 设置为 false 也会将 renderLayers 设置为 false,然后向内容进程发送消息以将其顶级 docShell 活动状态设置为 false

对于非远程 <xul:browser>docShellIsActive 将转发到 <xul:browser> 的顶级 docShell 上的 isActive 属性。

将 docShell 设置为活动状态会导致选项卡的 visibilitychange 事件触发,以指示选项卡已变为可见。在选项卡被选中之前一直等待播放的媒体也将开始播放。

生成已加载文档的打印预览也需要活动 docShell。

需求

选项卡切换器必须满足许多需求。它们按无特定顺序排列:

  1. 切换器必须准备好切换任何远程和非远程选项卡的混合。非远程选项卡包括指向 about:addonsabout:config 和其他选项卡的选项卡

  2. 我们希望避免在准备好显示我们要切换到的选项卡的图层之前切换工具栏状态(例如,URL 栏输入、安全指示器、工具栏按钮状态)

  3. 在任何给定时间,选项卡栏中应该只显示一个选项卡处于选中状态

  4. 我们希望避免在选项卡的图层准备好之前将键盘焦点切换到选定的选项卡 - 但前提是用户在异步选项卡切换的开始和结束之间没有更改焦点

  5. 如果选项卡的图层在一段时间后不可用,我们应该通过显示“选项卡切换微调器”(在白色背景上显示的动画微调器)来“完成”选项卡切换。这样,我们至少向用户显示了一些活动,尽管我们没有要显示的选项卡图层

  6. 打印 UI 使用选项卡显示打印预览,这需要打印预览选项卡处于后台,但其 docShell 也必须处于“活动”状态 - 通常为选定选项卡保留的状态。请参阅 renderLayers、hasLayers、docShellIsActive

  7. <xul:tab><xul:browser> 可以在异步选项卡切换期间的任何时间创建或销毁

  8. 即使未将选项卡设置为活动状态,也应该可以渲染选项卡的图层(这用于 预热

生命周期

每个窗口的异步选项卡切换器实例仅当一个或多个选项卡仍需要加载或卸载其图层时才存在。这意味着即使选项卡切换对用户来说似乎已完成,异步选项卡切换器实例也可能存在。这也意味着如果用户在一些后台选项卡的图层卸载之前启动新的选项卡切换,则异步选项卡切换器可能会继续存在并处理新的选项卡切换。

每个窗口一次只有一个异步选项卡切换器,它由 <xul:tabbrowser> 拥有。

<xul:tabbrowser> 从没有异步选项卡切换器开始,只有当用户启动选项卡切换(或预热)时才会实例化切换器。

一旦切换器确定用户请求的选项卡正在显示,并且所有后台选项卡都已正确卸载或销毁,异步选项卡切换器就会清理并自行销毁。

选项卡状态

在异步选项卡切换器存在期间,它会将窗口中的每个 <xul:tab> 映射到以下内部状态之一:

STATE_UNLOADED

<xul:tab> 的图层未上传到合成器,我们也没有请求选项卡开始这样做。此选项卡完全位于后台。

当选项卡处于 STATE_UNLOADED 状态时,这意味着关联的 <xul:browser> 要么不存在,要么其 renderLayershasLayers 属性都会返回 false

如果选项卡处于此状态,则它必须是在此处初始化的,或者从 STATE_UNLOADING 状态转换而来。

在记录状态时,此状态由字符串 unloaded 表示。

STATE_LOADING

<xul:tab> 的图层尚未报告为合成器“已接收”,但我们已请求选项卡开始渲染。这通常意味着我们想要切换到选项卡,或者至少预热它。

当选项卡处于 STATE_LOADING 状态时,这意味着关联的 <xul:browser>renderLayers 属性将返回 true,而其 hasLayers 属性将返回 false

如果选项卡处于此状态,则它必须是在此处初始化的,或者从 STATE_UNLOADED 状态转换而来。

在记录状态时,此状态由字符串 loading 表示。

STATE_LOADED

<xul:tab> 的图层在合成器上可用,可以显示。这意味着选项卡要么正在显示给用户,要么可以非常快速地显示给用户。

如果选项卡处于此状态,则它必须是在此处初始化的,或者从 STATE_LOADING 状态转换而来。

当选项卡处于 STATE_LOADED 状态时,这意味着关联的 <xul:browser>renderLayershasLayers 属性都会返回 true

在记录状态时,此状态由字符串 loaded 表示。

STATE_UNLOADING

<xul:tab> 的图层曾经在合成器上可用,但我们已请求选项卡卸载它们以节省内存。这通常意味着我们已切换离开此选项卡,或已停止预热它。

当选项卡处于 STATE_UNLOADING 状态时,这意味着关联的 <xul:browser>renderLayers 属性将返回 false,而其 hasLayers 属性将返回 true

如果选项卡处于此状态,则它必须是在此处初始化的,或者从 STATE_LOADED 状态转换而来。

记录状态时,此状态由字符串 unloading 表示。

标签渲染其图层是通过将其状态设置为 STATE_LOADING 来完成的。一旦收到图层,切换器将自动将状态设置为 STATE_LOADED。类似地,告诉标签停止渲染是通过将其状态设置为 STATE_UNLOADING 来完成的。一旦图层完全卸载,切换器将自动将状态设置为 STATE_UNLOADED

逐步完成简单的标签切换

在我们简单的场景中,假设用户有一个带有两个标签的单个浏览器窗口:索引为 0 的标签和索引为 1 的标签。这两个标签都已完全加载,并且当前选中了 0 并显示其内容。

用户选择切换到标签 1。实例化了一个异步标签切换器,它立即将多个事件处理程序附加到窗口。其中包括 MozLayerTreeReadyMozLayerTreeCleared 事件的处理程序。

然后,切换器创建了一个从 <xul:tab>> 到状态的内部映射。该映射为

// This is using the logging syntax laid out in the `Tab states` section.
0:(loaded) 1:(unloaded)

请务必参考 标签状态 以了解术语的解释,以及 日志记录 状态的语法。

最后一个示例转换为

索引为 0 的标签处于 STATE_LOADED 状态,索引为 1 的标签处于 STATE_UNLOADED 状态。

现在初始化已完成,切换器被要求请求 1。它通过将 1 设置为 STATE_LOADING 并请求渲染 1 的图层来做到这一点。新的状态映射为

0:(loaded) 1:(loading)

此时,用户仍在查看标签 0,并且没有任何 UI 显示任何可见的标签更改指示。

现在切换器正在等待,因此它返回到事件循环。在此期间,如果任何代码要询问 tabbrowser 哪个标签被选中,它将返回 1,因为它在逻辑上被选中,尽管在视觉上未被选中。

最终,1 的图层上传到合成器,并且 1<xul:browser> 触发其 MozLayerTreeReady 事件。此时,切换器再次更改其内部状态

0:(loaded) 1:(loaded)

因此,现在 01 的图层都在上传,并且可在合成器上使用。此时,切换器更新浏览器的视觉状态,并将 <xul:deck> 翻转以显示 1,用户体验标签切换。

但是,切换器尚未完成。在预定义的时间段后(由 UNLOAD_DELAY 决定),当前未选中但处于 STATE_LOADED 状态的标签将被置于 STATE_UNLOADING 状态。现在内部状态如下所示

0:(unloading) 1:(loaded)

在请求 0 进入 STATE_UNLOADING 后,切换器返回到事件循环。同时,用户继续使用 1

最终,0 的图层从合成器中清除,并且 0<xul:browser> 触发其 MozLayerTreeCleared 事件。此时,切换器再次更改其内部状态

0:(unloaded) 1:(loaded)

0 处的标签现在处于 STATE_UNLOADED 状态。由于最后一个请求的标签 1 处于 STATE_LOADED 状态,并且所有其他后台标签都处于 STATE_UNLOADED 状态,因此切换器认为其工作已完成。它取消注册其事件处理程序,然后自毁。

卸载后台标签

在存在异步标签切换器的情况下,它将定期扫描窗口中处于 STATE_LOADED 状态但也在后台的标签。然后,这些标签将被置于 STATE_UNLOADING 状态。只有在所有后台标签都进入 STATE_UNLOADED 状态后,才认为后台标签已完全清除。

后台扫描间隔为 UNLOAD_DELAY(以毫秒为单位)。

感知性能优化

我们使用一些技巧和优化来帮助提高标签切换的感知性能。

  1. 有时用户会在相同的标签之间快速切换。我们希望通过在一段时间后才释放标签的图层来优化这种情况。这样,快速切换只需在合成器中解决重新合成,而不是远程标签的内容进程的完整重绘和图层重新上传。

  2. 当以前从未见过标签并且仍在加载过程中时(现在,通过查找 <xul:tab> 上的“忙碌”属性来进行可疑检查),我们显示一个空白内容区域,直到其图层最终准备好。这里的想法是将感知延迟从异步标签切换器转移到网络,通过显示空白空间而不是标签切换微调器。

  3. “预热”是一种新兴的优化,它允许我们预先渲染和缓存我们认为用户很快可能会切换到的标签的图层。在超时后(browser.tabs.remote.warmup.unloadDelayMs),未切换到的“预热”标签的图层将被卸载并从缓存中清除。

  4. 在支持 occlusionstatechange 事件(截至撰写本文时,仅限 macOS)和 sizemodechange 事件(Windows、macOS 和 Linux)的平台上,当窗口最小化或被另一个窗口完全遮挡时,我们将停止渲染当前选中标签的图层。

5. 基于 browser.tabs.remote.tabCacheSize 首选项,我们保留最近使用的标签的图层以加快标签切换速度,避免往返于内容进程。这在 tabbrowser.js 中使用一个简单的数组(_tabLayerCache),我们在确定是否要卸载标签的图层时会检查它。截至 Nightly 62,这仍在试验阶段。

预热

标签预热允许浏览器主动渲染并上传用户可能切换到的标签的图层到合成器。最简单的例子是当用户的光标悬停在标签上时。发生这种情况时,异步标签切换器被告知将该标签放入预热列表,并将其状态设置为 STATE_LOADING,即使用户尚未单击它。

预热标签会启动一个计时器以卸载后台标签(如果尚不存在此类计时器),如果用户最终未单击该标签,则该计时器将清除预热标签。即使用户继续悬停标签,卸载也会发生。

如果用户确实单击了预热标签,则该标签可能处于以下两种状态之一

STATE_LOADING

在这种情况下,用户在渲染图层并由合成器接收之前请求了标签切换。我们至少会减少预热和选择之间的时间,以便向用户显示标签的内容。

STATE_LOADED

在这种情况下,用户在渲染图层并由合成器接收后请求了标签切换。我们可以立即切换到该标签。

预热由以下首选项控制

browser.tabs.remote.warmup.enabled

是否启用预热优化。

browser.tabs.remote.warmup.maxTabs

可以同时预热的标签的最大数量。如果预热标签的数量超过此数量,则所有后台标签都将被卸载(请参阅 卸载后台标签)。

browser.tabs.remote.warmup.unloadDelayMs

第一个标签被预热到卸载所有后台标签之间等待的时间(请参阅 卸载后台标签)。

日志记录

异步标签切换器具有一些日志记录功能,使调试和推理其行为变得更容易。将隐藏的 browser.tabs.remote.logSwitchTiming 首选项设置为 true 会将日志记录放入浏览器控制台。

或者,在标签切换器的源代码中将 useDumpForLogging 属性设置为 true 会将这些日志转储到标准输出。