广州淘宝美工

网络科技

全职美工 译文: 通过增量帧加载提高性能

发布日期:2024-08-15 10:28    点击次数:166

全职美工 译文: 通过增量帧加载提高性能

大型原型需要几分钟才能加载,用户注意到了这一点。以下是我们如何彻底改造原型播放器以缩短加载时间和提高稳定性。

插图由 Enle Li 绘制。

当我们首次构建原型设计功能时,我们有一个目标:让用户在 Figma 中创建交互式流程。从技术角度来看,我们构建了一个简单的加载策略来匹配。我们会将包含原型的整个文档加载到内存中,然后再显示起始屏幕。

移动设备(尤其是 iPhone)的内存往往比台式设备要少,操作系统通常会终止我们的进程,而不是扩大应用程序的内存预算。

这在当时是合理的。但随着 Figma 变得越来越先进,文档变得越来越大,包含许多页面和设计系统,组件变体的数量呈指数级增长。原型的大小和规模成倍增加,而加载时间和稳定性却落后了。同样,原型经常在移动设备上崩溃,超出了移动设备内存限制。

增量加载是一个概念,用于描述仅加载新的或更新的数据,而不是加载整个文件。通过仅同步更新,流程可以更高效地运行。增量“框架”加载是团队创造的一个术语,用于描述我们如何将这些流程应用于 Figma 的原型。

很明显,数据量太大才是罪魁祸首。如果我们不将整个原型加载到内存中,而是只加载显示当前屏幕上的内容,会怎么样?使用这种增量帧加载策略,我们可以一举两得,通过仅下载和存储所需的内容,解决加载时间和内存使用问题。

仔细考虑

在我们的规模上保持流畅的用户体验将涉及增强我们的多人系统以允许逐段同步文档内容。

这听起来很简单,但 Figma 的真正复杂之处在于它拥有内置于浏览器中的多人游戏技术,以及丰富的功能。如果我们不再加载整个原型,那么我们实际上应该预先加载多少原型?我们什么时候应该加载更多?

可交互时间衡量页面完全可交互所需的时间。这是由页面显示有用内容并在 50 毫秒内开始响应点击和其他用户交互的最早时间定义的。

要回答这个问题,我们需要考虑查看原型的体验。我们希望用户能够尽快与原型进行交互。要开始与所需的最少屏幕进行交互,我们只需要第一个屏幕和原型交互可以到达的任何相邻屏幕。因此,只需加载这部分屏幕即可阻止交互时间。当用户导航到下一个屏幕时,我们将加载其相邻屏幕,依此类推!

用户开始查看第一帧;我们预加载其可立即到达的帧。

用户导航并查看下一帧;我们预加载新帧的可立即访问的帧。我们还保持第一帧的加载状态不变,因为用户可以导航回它。

另一个考虑因素是 Figma 文件可以实时更新,任何更改都会立即同步到文档的所有活动查看者。这使得设计人员可以轻松地在目标设备上查看流程,同时在另一台设备上迭代底层文档。在我们之前的系统中,实时更新通过将每个文件与我们服务器上的实时数据存储相关联来实现。为了允许客户端仅加载文件的子集,我们添加了对查询文件特定部分的支持。

通过指定数据库与客户端通信的协议(考虑到文件内容或客户端查询的变化)全职美工,可以实时进行查询。从高层次来看,此文档同步协议如下所示:

服务器会话以空订阅开始。客户端请求一条query消息,指定它想要订阅的文档树中的子树。

服务器响应一条reply包含子树快照的消息,并确认query已完成。

在初始响应之后,服务器将通过附加的“ changes”消息将任何后续更新同步到订阅的子集。

以下是一个例子:

客户端发送一个query。a这意味着“让我订阅 ID 为 的节点a,以及它的所有祖先和所有后代。”

服务器将发送reply查询a已完成的确认信息以及查询的内容。

将来每当这些节点发生变化时,服务器就会发送更多changes消息。

我们首先让客户端订阅服务器上显示的完整文件的子集 (a、b、c、x)。因此,当 c 的属性发生变化时,客户端会收到有关该变化的消息。

接下来,发生更改,将节点 y 重新设置为节点 a 下的父节点。由于客户端订阅了 a,因此服务器会发送一条消息,通知客户端 a 下存在节点 y。

最后,c 被重新设为 x 的父级,并且客户端不再订阅 x 的后代。客户端收到一条 c 被删除的消息。

其他值得注意的潜水

上述协议是我们增量帧加载方案的核心,但还需要一些额外的细节来支持所有文件和案例。以下是我们认为最有趣的几个细节。

依赖边

Figma 中的某些对象具有需要从其他对象推断的属性。例如,实例是其支持组件的副本:每当支持组件发生更改时,实例也需要更新。同样,当样式或变量本身发生更改时,样式或变量的使用者也需要更新。为了支持这一点,当客户端订阅某个对象时,我们的协议也会以传递方式订阅其依赖项(以及它们的任何依赖项)。

为了使事情变得更加复杂,美工兼职依赖关系会随着文档的编辑而改变:节点可以重新定位,可以切换它们使用的样式,等等。

当添加客户端订阅中尚不存在的新依赖项时,服务器会发送node created更改,以便客户端了解这些节点。

当从订阅中删除旧的依赖项时,服务器会发送node removed更改,从而有效地允许客户端从内存中“驱逐”那些节点。

更改订阅

当客户端浏览 Figma 原型时,它们会更改其订阅的节点集。这意味着在客户端浏览时,子树也会“取消订阅”。如果查看者取消订阅某个节点,但稍后又重新订阅该节点,会发生什么情况?如果受影响的节点在此期间被更新,甚至被删除,如果我们不小心,客户端最终可能会得到过时的节点。通过始终从内存中逐出取消订阅的节点并确保服务器在重新订阅时发送这些节点的新版本,我们可以确保这种情况永远不会发生。

这对于保持低内存使用率也很重要,正如我们将在下面看到的。

优化原型设计体验

这种用于同步 Figma 文档的新协议使得增量加载原型成为可能,同时仍支持实时更新。现在,我们要做的就是将客户端连接到此协议,以提供增量加载的原型体验。我们设计了全新的客户端策略,目标是减少内存使用量和加载时间,同时保留现有的、快速的导航。

减少内存使用量和加载时间

增量加载允许我们订阅原型正常运行所需的最少屏幕数量。请记住:这意味着我们只需要原型的第一个屏幕以及任何相邻的屏幕。

在线设计师

接下来,我们希望确保即使客户端在原型中导航,它们也不会在内存中积累不必要的数据。我们确保客户端会驱逐任何未直接订阅的屏幕,因此客户端在导航原型时请求新屏幕时会不断清理未使用的屏幕。然而,正如我们将在下一个目标中看到的那样,不断分配和驱逐屏幕与保持流畅的导航体验相矛盾。

保留现有的导航体验

减少内存使用量并非免费!由于 Figma 客户端现在最初加载的屏幕更少,这意味着我们需要在以后偿还这一代价——当用户向前导航并需要下载新屏幕时。回想一下,我们的客户端策略只加载当前正在查看的屏幕旁边的屏幕。这意味着如果用户连续进行两次快速导航,他们将需要下载更多屏幕并会出现加载旋转器。

那么,为什么我们不继续在后台加载更多原型呢?这直接损害了我们的一个主要目标,即减少移动设备上的内存使用量。我们最终会加载整个原型并再次导致应用程序崩溃。为了解决这个问题,我们决定在桌面和移动设备之间分叉行为:在桌面上,我们将继续在后台加载所有屏幕以保持快速和流畅的导航;在移动设备上,我们会在用户导航时加载屏幕,逐出足够旧的屏幕以减轻内存消耗。

另一个障碍是后台处理负载导致的锁定。在增量加载中,当用户从一个屏幕导航到下一个屏幕时,会发生很多事情:我们的渲染器播放动画,我们计算有关新屏幕的元数据,我们预加载并处理更多屏幕。在测试中,我们了解到用户的设备经常会在所有这些工作中锁定。动画和交互运行不顺畅,这不符合我们的质量标准。这个问题很难解决,因为 Figma 必须在或多或少单线程的 Web 浏览器环境中运行。我们无法将负载处理委托给工作程序或以其他方式利用并行性,因为我们只需要读取和写入主线程中的状态。

为了减轻影响,我们对原型体验进行了广泛的优化。首先,我们将文件的每次增量加载拆分为更小的块,这样可以更轻松地在后台逐步加载。然后,如果渲染元数据(例如布局和布尔运算)未显示给用户,我们会跳过这些元数据的计算。加上对其他昂贵流程的有针对性的优化,这些措施大大减少了用户体验到的 CPU 锁定量。

经过仔细规划和权衡后,我们向所有用户推出了这些改进。从那时起,我们很高兴看到不仅加载时间有所改善,而且移动设备上的崩溃率也有所降低。与 Figma 的其他工程工作一样,我们优先考虑用户体验,接受随之而来的复杂性,并投资创建最佳架构以满足这些需求。

作者:

Matthew HuangSoftware Engineer, Figma

Andrew ChanSoftware Engineer全职美工, Figma

特别声明:以上内容(如有图片或视频亦包括在内)来源于网络,不代表本网站立场。本网站仅提供信息存储服务。如因作品内容、版权和其他问题需要同我们联系的,请联系我们及时处理。联系方式:451255985@qq.com,进行删除。