[译] Github Actions 是如何渲染超大日志的

Github Actions 是如何渲染超大日志的

在 web 端渲染日志看起来很简单:它们只是一行一行的纯文本。但是它也可以有许多方便用户的附加功能:着色、分类、搜索、永久链接等等。但是最重要的是,不管日志只有十行还是数万行,显示日志的用户界面都应该都能正常运作。这是我们从 2019 年向公共开放 GitHub Actions 时就优先关注的。我们那时候还没有用上使用量指标,但我们需要应对超长日志是显而易见的。浏览器可能在初次加载时卡住,处置不当的话也很可能导致其完全无法使用。我们必须应用一个叫虚拟化的技术。

虚拟化是指只渲染列表中的一部分内容。这既可以让界面顺畅地运作,又不会让用户发现可见视窗外的内容并没有被渲染。它需要在用户滚动的时候更新当前可见的内容,甚至在内容还没被渲染时就计算布局的位置,来保障一个顺畅的滚动体验。

初步实现

在我们首次发布 GitHub Actions 时,我们测试了一个基于 React 的和一个基于原生 JavaScript 的库。有些库因为其实现方式导致它们有很大的局限性。比如,很多的库都要求所有被渲染的条目有固定的高度,毕竟这个局限令它们的计算简单了很多,因为如果一个用户想滚动到一个特定的条目(无论是否可见),它们只需要计算 item_index * items_height 就可以计算出其位置并滚动过去。而且,在计算整个可滚动区域的高度时,它们同样只需要计算 items_count * items_height 即可。当然,在很多情况下并不是所有条目都拥有相同的高度,这使得这个局限性变得不可接受。在 GitHub Actions 这里,我们想让那些很长的行分行显示,这意味着我们需要支持高度可变的日志条目。

我们最终选择了基于一个原生 JavaScript 的库,最大程度的涵盖了我们所需的功能:支持渲染高度可变的元素,支持滚动到特定条目等等。然而因为各种各样的原因,我们开始发现一些漏洞和用户体验上的缺陷:

  • 可滚动区域需要有一个固定高度,这使得内部功能的实现变得简单,但同时也是一个典型的限制。但在我们的案例中,这令用户体验变得很差,特别在我们的日志渲染中,一个任务有数个步骤,并且每个步骤都有自己的列表需要虚拟化渲染。这意味着页面中的每个步骤都需要有自己的滚动区域,并且还需要有一个服务整个页面的滚动条。
  • 切换虚拟化列表的可见性,从隐藏转为可见,这个功能并没有经过一个良好的测试。在 GitHub Actions 中,我们允许用户展开和收起步骤,并且可以自动扩展日志。我们发现了一个bug,在步骤开始展开的时候。当日志自动展开变得可见,但那时网页标签并不可见(译者按:切到了其他标签),当用户在切回此页面后有时会看不到那些之前扩展开的日志。
  • 用户不能在滚动的同时选中文本,因为被选中的文字会因为虚拟化而被从 DOM 中移除。
  • 有些时候,我们需要在后台渲染日志行来计算它们的高度,这使得体验变得很卡。虚拟化并没有帮到我们太多,实际上有些行并没有完全不被渲染,反而会被渲染两次。但我们必须这么做,因为错误的高度计算会导致日志行被界面截断。

基于实际使用重新审视日志体验

基于这些原因,我们决定改进日志体验。我们从一个问题开始:我们是否还需要虚拟化?之前开始时提到的,我们没有使用的指标,但我们现在可以依据真实使用来做决策了。例如,如果大部分用户的日志都小到不需要虚拟化就可以顺利渲染,那我们就可以移除虚拟化,但允许单独下载更大的日志文件。

我们的数据显示 99.51% 的现有任务都不超过 50000 行,但我们知道浏览器在渲染超过 20000 行的日志时会开始吃力。同时我们发现,有的时候即使日志行数很少,也可能会占用大量的内存。有了这些信息,我们决定我们不需要数据虚拟化,不过依然需要用户界面的虚拟化。应用数据虚拟化会让我们只加载部分日志到内存中,并在用户滚动时继续加载更多信息,然而我们发现这种级别的复杂度是没有必要的。在那些日志文件非常大但行数很少的边缘情况,我们会截断这段日志,并提供一个下载日志的链接。

从头实现我们自己的虚拟化库

做出这些决定后我们立刻尝试了一些其他之前没有用到的库,但没有一个库能满足我们的需求。我们需要从头开始实现一个自己的。我们的目标有:

  • 可以渲染至少 50000 行的日志,最好也可以在手机上渲染出来。
  • 允许用户不受限地选中文本。
  • 让用户界面/体验在大部分情况下都能保持流畅。这包括在搜索结果或永久链接间跳转和流播日志时。
  • 只使用一个可滚动区域且高度无需固定。
  • 在步骤上使用黏性表头。
  • 让计算越快越好,而且使用更少内存。不完全精准的计算是可以接受的。

为了达到这些目标,我们需要与其他库在几个方面采用不同的手法:

  • 在渲染前预估高度:其他虚拟化库依赖于精确的计算或固定高度,并使用绝对位置来使内部实现变得很简单。我们决定在渲染前先预估高度来避免复杂的计算。我们同时使用相对位置来避免日志行因为计算不精确而被截断。
  • DOM 结构:另一个挑战是如何组织 DOM 来在只有一个可滚动区域的条件下允许粘性表头。该滚动容器不仅要满足这两个条件,且粘性表头不能被虚拟化。

我们做了一个快速的实现来验证我们的策略。在经过一些测试生成大型日志后,我们发现,尽管证明了我们可以用自己的实现方式来满足所有目标,但是我们必须非常小心,因为很容易就会犯错并毁掉整个用户体验。例如,我们很快就发现,改变尽可能少的 DOM 节点固然重要,但是在滚动时尽可能减少 DOM 的变化也是非常重要的。当用户滚动时,我们需要加入变得可见的节点,并移除那些已经不在可见视图中的。如果用户滚动速度很快,尤其是在移动设备上,很容易就会导致过多的 DOM 变化,并带来不良的体验。

然而我们也有几个办法来解决这个问题。比如,你可以使用节流来分批次缓速更新。但我们发现这种方法会让用户界面变得不流畅。最后我们想的办法是将日志行分块,因此在增加和移除内容时,我们不操作单独的行,而是操作由 N 行组成的块。经过一些测试,我们得出了一个块应该有多少行:50。

生产就绪

我们用了大概一周时间来完成初步的实现,该实现已经让我们看到了诸多用户体验上的改良。此时我们已经知道自己在正确的道路上了。接下来的几周里,我们继续完善用户界面和体验,并且我们知道我们会有一大堆边缘案例需要处理。

经过大量的内部工作,我们将其交付给了用户,并很高兴可以提供更优质的日志体验:更快、更流畅、更好用,而且更完整和健壮。大部分时间你不需要重新发明轮子,但有些时候最好的解决方案就是自己从头实现的解决方案,这样才能够完全地掌控产品的体验和性能。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享