无限滚动游戏的复杂性

罗伯特·弗拉克
Robert Flack

要点:重复使用您的 DOM 元素并移除离视口较远的元素。使用占位符考虑延迟的数据。您可以点击此处查看这款无限滚动游戏的演示代码

无限滚动条会在整个互联网上层出不穷。其中有 Google 音乐的音乐人列表,Facebook 的时间轴,Twitter 的实时 Feed 中也有。您向下滚动,在滚动到底部之前,新内容会神奇地看似无处不在。它可以为用户提供顺畅的体验,并且易于发现吸引力。

不过,无限滚动游戏背后的技术挑战远比看起来更困难。当您想做 The Right ThingTM 时,会遇到各种各样的问题。它从简单的事情开始,例如页脚中的链接变得几乎无法访问,因为内容总是将页脚推开。但问题会变得更加困难。当有人将手机从竖屏转为横屏时,您如何处理调整大小事件,或者当列表过长时,您如何防止手机因手机磨损而痛苦地暂停?

正确之事 TM

我们认为,这足以让我们想出一个参考实现,展示一种在保持性能标准的同时,以可重复使用的方式解决所有这些问题的方法。

我们将使用 3 种技术来实现我们的目标:DOM 回收、Tombstone 和滚动锚定。

我们的演示版将是一个类似于 Hangouts 的聊天窗口,我们可以在其中滚动浏览消息。首先,我们需要一个无限来源的聊天消息。从技术上讲,所有无限滚动条都没有真正意义上的无限滚动,但如果有大量数据可用来注入这些滚动条,那么它们很可能是无限的。为简单起见,我们将只对一组聊天消息进行硬编码,并随机选择消息、作者和偶尔的图片附件,同时添加一点人工延迟,以使行为更接近真实网络。

Chat 应用屏幕截图

DOM 回收

DOM 回收是一种用于减少 DOM 节点计数的技术未得到充分利用。一般思路是使用已创建的屏幕外 DOM 元素,而不是创建新的元素。不可否认的是,DOM 节点本身虽然开销很低,但并不是免费的,因为每添加一个节点,都会增加内存、布局、样式和绘制方面的开销。如果网站的 DOM 太大而难以管理,低端设备如果并非完全无法使用,运行速度会明显变慢。另请注意,对于样式的每次重新布局和重新应用(每当在节点中添加类或从中移除类时都会触发一个过程),使用更大的 DOM 会使开销相应增加。回收 DOM 节点意味着我们将大幅减少 DOM 节点的总数,从而提升所有这些进程的运行速度。

第一个障碍是滚动本身。由于在任何给定时间,我们在 DOM 中的所有可用项中都只有很小一部分,因此我们需要找到另一种方法来使浏览器的滚动条正确反映理论上该处的内容量。我们将使用带有转换的 1px x 1px 标记元素,强制包含项目的元素(跑道)具有所需的高度。我们会将 T 台中的每个元素提升到其自己的层,以确保 T 台本身的层完全为空。没有背景颜色 也没有声音如果跑道的图层是非空的,就不符合浏览器优化的条件,我们必须在显卡上存储高度为几十万像素的纹理。在移动设备上绝对是不可行的

每次滚动时,我们都会检查视口是否足够接近跑道末端。如果是这样,我们将通过移动标记元素并将离开视口的项移动到跑道底部来延长跑道,并用新内容进行填充。

跑道 Sentinel } }

向另一个方向滚动时也是如此。不过,我们绝不会在实现中缩减 T 道,以便滚动条位置保持一致。

Tombstone

如前所述,我们尝试让数据源像现实世界一样运行。包括网络延迟等各种问题。这意味着,如果用户使用滑动滚动,他们可以轻松滚动经过我们拥有相关数据的最后一个元素。如果发生这种情况,我们将放置一个 Tombstone 项(占位符),在数据到达后,该项将被替换为实际内容。Tombstone 也会被回收,并针对可重复使用的 DOM 元素使用单独的池。我们需要这样才能实现从 Tombstone 到填充内容的项的完美过渡,否则这会让用户非常刺眼,而且可能实际上会让他们忘记自己关注的内容。

这种墓。非常石头。哇!

这里的一个有趣的挑战是,由于每项文本量或附加图片的文本量不同,真实内容项的高度可能大于 tombstone 项。为了解决此问题,每当有数据进入并在视口上方替换 Tombstone 时,我们都会调整当前的滚动位置,从而将滚动位置锚定到某个元素,而不是像素值。这个概念称为滚动锚定。

滚动锚定

在替换 Tombstone 以及调整窗口大小时(也会在翻转设备时)调用滚动锚定功能。我们必须确定视口中最顶层的可见元素是什么。由于该元素只能部分可见,因此我们还会存储从视口开始处元素顶部的偏移量。

滚动锚定示意图。

如果视口大小经过调整且 T 台发生了更改,我们能够恢复呈现给用户的视觉效果,以及视觉效果。赢了!除了调整大小的窗口外,可能每个项的高度都可能改变了,那么我们如何知道应该将锚定内容放在多低的位置呢?我们做不到!我们必须对锚定项上方的每个元素进行布局,并将所有高度相加,才能发现这一点;这可能会导致大小调整后出现严重的暂停,我们不希望出现这种情况。相反,我们会假定以上每项内容的大小都与 Tombstone 相同,并相应地调整滚动位置。当元素滚动到 T 轨道中时,我们会调整滚动位置,从而有效地将布局工作推迟到实际需要的时间。

布局

我跳过了一个重要细节:布局。每次回收 DOM 元素时,通常都会重新布局整个跑道,这会远低于每秒 60 帧的目标。为避免这种情况,我们将自行承担布局的责任,并通过转换来使用绝对定位的元素。这样一来,我们就可以假设,实际上只有空白区域时,位于 T 台较高的所有元素仍然占据空间。由于我们自己是执行布局操作,因此可以缓存各个项最终到达的位置,并且当用户向后滚动时,我们可以立即从缓存中加载正确的元素。

理想情况下,这些项目只有在附加到 DOM 后才会重新绘制一次,并且不会因在 T 台中添加或移除其他项目而受到影响。这是可能的,但仅限于新型浏览器。

尖端技术优化

最近,Chrome 增加了对 CSS Containment 的支持,借助此功能,开发者能够告知浏览器某个元素是布局和绘制工作的边界。由于我们要自己来布置布局,因此它是适合容器的主要应用。每当我们向 T 台添加元素时,我们都知道其他项不需要受重新布局的影响。因此,每个项目都应该获得 contain: layout。我们也不想影响网站的其余部分,因此 T 台本身也应该采用此样式指令。

我们考虑的另一项工作是使用 IntersectionObservers 作为一种机制,用于检测用户何时滚动至足以让我们开始回收元素并加载新数据。不过,IntersectionObserver 指定为高延迟(就像使用 requestIdleCallback 一样),因此在使用 IntersectionObserver 时,实际响应速度可能会比不使用时低。即使我们当前使用 scroll 事件的实现也遇到了此问题,因为系统会“尽力而为”分派滚动事件。最终,Houdini 的合成器 Worklet 将是这个问题的高保真解决方案。

还不够完美

我们目前的 DOM 回收并不理想,因为它会添加所有通过视口的元素,而不是只关注实际显示在屏幕上的元素。这意味着,当您快速滚动时,Chrome 上的布局和绘制工作量会太大,无法跟上进度。最终您将只能看到背景。虽然这不是世界末日 但绝对值得改进

如果您想兼顾出色的用户体验和高性能标准,希望您见到会有多挑战性的简单问题。随着渐进式 Web 应用成为手机上的核心体验,这一点将变得更加重要,Web 开发者将不得不继续投资于使用遵循性能限制的模式。

所有代码都可以在我们的代码库中找到。我们已尽最大努力确保它可以重复使用,但不会在 npm 上将其作为实际库发布,也不会作为单独的代码库发布。该应用主要用于教育目的。