加速文件加载时间,逐页进行
2024 年 5 月 23 日

2024年5月22日

Isaac Goldberg,Figma软件工程师

加快文件加载速度,一个页面一个页面地进行

在我们的日常工作中,我们经常需要处理大量的文件和资源。为了优化用户体验并提高工作效率,我们努力使文件加载速度更快。在过去的几个月里,我们专注于改进文件加载时间,特别是在处理大型设计文件时。下面是我们所做的一些工作。

  1. 在 Figma 中预加载字体
  2. 延迟加载组件
  3. 优化大型文件的加载

我们希望这些改进措施能够帮助您更有效地使用 Figma,并带来更好的工作体验。感谢您的持续支持! Naomi Jung 软件工程师,Figma

FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUABQDASIAAhEBAxEB/ 1. 了解我们数据模型中的读取依赖关系 (opens new window) 2. 为查看器构建动态加载功能 (opens new window) 3. 解开写入依赖关系的纷乱复杂性 (opens new window) 4. [主题变体](https://www.figma.com/en-us/blog/speeding-up-file-load-times-o 1. 我们明显的动态方法 (opens new window) 2. 验证依赖关系 (opens new window) 3. 实践中的性能 (opens new window)

  1. 了解数据模型中的读取依赖关系 (opens new window)
  2. 为查看者构建动态加载 (opens new window)
  3. 解开写入依赖关系 (opens new window)
  4. [主题的变化](htt Figma文件通常庞大而复杂,拥有无尽的页面、库和本地组件以及原型屏幕。以下是动态页面加载如何将最慢的加载时间提升了33%。

对于最慢的5%页面加载,我们看到加载时间减少了33%。

每个工程团队都希望他们的产品感觉快速。除了为用户提供最佳体验外,加载速度有着根本性的影响,塑造了用户的第一印象。这对我们尤为重要。 设计师们通常整个工作日都在 Figma 上工作,因此节省几秒钟的增量性能改进在工作日内会逐渐累积。随着 Figma 的产品团队构建更多功能并且用户向他们的文件添加更多内容,我们的平台团队努力维护和改进性能。我们理想的加载时间趋势是“向下并向右”:我们希望随着文件增加,文件加载时间能够逐渐减少。

性能应该与用户感知的复杂性相对应。如果用户加载一个只有几个帧的页面,Figma 应该能够几乎立即显示他们的画布,即使文件中有数十个页面,每个页面都有数百个帧。通过考察用户的使用模式,我们发现许多用户将文件视为项目——使用一个文件来容纳工作流的所有方面——因此大多数用户甚至在单个会话中都没有浏览所有页面。我们意识到,通过根据需要动态加载内容,而不是弹出窗口,可以显著改善加载时间并减少内存。 在我们的数据模型中理解读取依赖关系

作为性能优化的动态加载并不是一个新概念,但在 Figma 的基于浏览器的文件结构中,我们需要解决一些特定的独特挑战。广义上说,Figma 文件是一个节点树,其中每个节点都是一个具有属性的可交互层。重要的是,某些节点可能引用“其他页面”的节点。这意味着节点之间可能存在许多跨页面的依赖关系,从明显的到非常复杂的依赖关系,我们都需要考虑。

在 Figma 的数据模型中,一个实例包含对另一个节点的指针,该实例的背景组件可能位于另一页。我们将这种边缘称为“读取依赖关系”,即实例对组件有读取依赖关系。为了正确渲染实例,客户端必须首先下载组件的节点。 组件是您可以在设计中重复使用的元素。它们有助于在项目之间创建和管理一致的设计。实例是组件的链接副本,会自动接收组件所做的任何更新。

除了组件和实例,许多 Figma 功能涉及读取依赖项。例如,Figma 将样式 (opens new window)实现为用户不可见节点。如果一个框架使用“BrandPrimary”填充样式 #FFD966,为了呈现框架,客户端必须首先下载相应的样式节点。变量 (opens new window)也是类似的。如果一个节点将“text-subheader”大小变量应用于其字体大小,客户端需要访问变量节点以解析正确的原始值(例如 16)用于字体大小。 以往对于仅查看文件和原型的动态加载的迭代使我们能够解决这些挑战。我们创建了一个名为 QueryGraph 的依赖框架,它被表示为一个内存图,连接着依赖节点之间的边,是 Figma 的多人协作技术 (opens new window) 的组成部分,负责跟踪向连接的客户端发送文件的哪一部分。QueryGraph 及其读取依赖的图形是仅供查看用户更快地加载文件和原型的基础。在每种情况下,动态加载的单元对应于我们在初始文件加载时呈现的内容类型:

  • 按 Figma 画布中的页面: Figma 不会加载整个文件,而是首先加载选定的页面,并按需加载其他页面。使用 QueryGraph,Figma 的多人协作系统将请求的页面发送给客户端,并加载任何所需内容。 在原型中的帧加载方面:Figma为原型查看者动态加载帧 (opens new window),每次仅显示一个屏幕。Figma还会预加载帧,您可以在当前状态下通过一定数量的过渡到达的帧,以防止明显的延迟。QueryGraph会确保客户端拥有Figma文件的所有必需部分,而没有多余的部分。

尽管查看者享受到动态加载的好处,但我们每天70%的文件加载来自可编辑文件。我们希望在通过逐页动态加载原型取得成功的基础上,将相同的加载逻辑扩展到可编辑文件。挑战在于确保当编辑影响尚未加载的页面上的组件时,变更正确传播到未加载的内容。为了向所有人交付这一优化,我们不能简单地重用现有的阅读逻辑。 或 editors to be more dynamic. This enhancement required us to expand our dependency-tracking system to accommodate a new type of edge. 在查看或渲染 Figma 文件的部分时,需要读取依赖项;而在文件的某些部分上编写或编辑则需要写入依赖项;在许多情况下,写入依赖项是现有读取依赖项的反向。这是因为 Figma 会缓存昂贵计算的结果,比如文本布局算法。如果样式发生变化,Figma 将重新运行文本布局并缓存派生数据结果,以便随时准备好进行渲染。例如,如果文本样式控制文本层的字体,这就是从文本层到样式节点的读取依赖项。反之则是写入依赖项:样式节点对文本层有写入依赖,这表明为了安全编辑样式节点,Figma 需要下游文本层。

这对于支持编辑器的动态加载至关重要。当用户... 编辑Figma文件的一部分,比如更改文本样式,客户端必须能够访问所有需要更新的下游对象,比如文本层的缓存字形信息。

以下是Figma数据模型中写入依赖关系的几个示例:

  • 组件对其所有实例具有写入依赖关系,这些实例可能位于其他页面上。当用户编辑组件时,Figma需要传播和更新组件实例的缓存。文本样式和变量也存在同样的关系。
  • 自动布局 (opens new window)意味着编辑一个节点的大小或位置也可能改变另一个节点的大小和位置。因此,这也是一种写入依赖关系。对于组件和实例,这种自动布局的写入依赖关系可能跨越页面。(例如,在一个页面上更新组件可能导致另一个页面上实例大小调整,进而可能导致实例的兄弟节点调整)。 对于自动布局框架,可以调整大小或重新排列。

两个按钮图标,一个名为“组件”带有实线边框,另一个名为“实例”带有虚线边框。 考虑了几种不同的动态加载编辑器的方法,有一个明显的领先者:编写依赖计算,这意味着更新 Figma 的多人游戏系统以考虑写入依赖关系。这类似于我们为仅查看用户和读取依赖关系所做的操作。 但仅查看的情况本质上风险较小。如果仅查看用户的加载逻辑有误,查看者可能会看到一个不正确的文件,但文件的完整性本身并不会受到威胁。但对于编辑用例,如果加载逻辑不包括正确的依赖关系,存在数据不一致会损坏文件数据的风险。(想象一下进行更改,却从未在另一页上看到那些更改的反映!)鉴于定义写入依赖关系的复杂性,我们还考虑了另外两种更简单的解决方案:延迟编辑和补充,以及数据模型的彻底改革。

延迟编辑和补充 #

这种方法将涉及使用与查看者的动态加载相同的逻辑加载第一页。一旦第一页加载完成,用户就可以平移、缩放和检查其内容。在后台,Figma将继续像今天一样加载完整文件。问题在于在其余的文件加载完成之前该文件仅可供查看。 在用户想要进行编辑和实际进行编辑之间可能会有延迟,因为数据需要从服务器下载。这可能会引入复杂的加载逻辑:我们需要在后台继续加载页面,并且如果用户导航到某个页面,仍然需要将该页面移到加载队列的前面。能够在不引入帧延迟或用户可感知的延迟的情况下填充如此大量的数据会带来挑战。

数据模型改进 #

写入依赖关系代表派生数据。当用户编辑依赖关系时,我们当前的系统使用推送模型来传播更新到其他节点,然后重新计算其派生数据缓存。我们考虑将这些系统迁移到使用拉取式的响应模型,这样我们就不需要在用户编辑依赖关系之前预先下载依赖节点。但是以这种方式彻底改造我们的系统将是一项巨大的工程,我们希望能够消除这些担忧。 我们决定采取的动态方法

任何一种替代方案都无法在短期内实质性地改善用户体验,同时又能成为可持续的长期解决方案。延迟编辑和回填在技术上更简单,但无法减少客户端内存。数据模型的彻底改变将避免对写入依赖性的需求,但这需要更长时间来完成。因此,我们选择了计算写入依赖性,这样可以在性能、可行性和用户体验之间取得平衡。采用这种加载方法,Figma在初始加载时下载第一页及其所有的读取和写入依赖项。当用户浏览到其他页面时,Figma会根据需要下载这些页面(及其读取和写入依赖项)。 以前,QueryGraph只编码读取依赖关系,因为查看器和原型不需要考虑写入依赖关系。为了将这个框架扩展到编辑器,我们用双向图替换了底层数据结构。在动态加载文件时,我们需要快速确定给定节点的两组依赖关系。

我们引入的自动布局写入依赖关系是节点之间隐式写入依赖的一个例子,这些节点不直接相互引用。我们在图中将这些依赖关系编码为一种新类型的边。

此外,所有现有的读取依赖关系都是外键依赖关系,这意味着依赖关系被明确编码在节点数据结构上。例如,实例节点有一个componentID字段,这提供了查找它们依赖的组件节点的外键。对编辑器的动态加载需要进一步扩展这一点以支持。 隐式写依赖关系,例如在自动布局框架中对节点进行编辑可能会导致相邻节点自动更改。

多人游戏同时在内存中保存文件的完整表示和依赖的QueryGraph,以便为客户端文件加载和编辑提供服务。对于每个动态文件加载,客户端指定初始所需页面,QueryGraph计算客户端需要的文件子集。用户对文件进行编辑时,服务器根据会话的订阅集和QueryGraph计算需要发送哪些编辑到每个会话,例如,如果用户只加载了文件中的第一个页面,并且协作者在另一页上更新了矩形的填充,我们不会将更改发送给第一个用户,因为从他们的订阅集中无法访问该矩形。 此Markdown内容包括有关显式和隐式写依赖关系的信息。显式写依赖关系是在代码中直接声明的依赖关系,而隐式写依赖关系是在代码中没有明确声明但确实存在的依赖关系。显式写依赖关系使得代码更易于理解和维护,而隐式写依赖关系可能会导致不必要的复杂性和错误。因此,在开发过程中,了解和管理这些依赖关系至关重要。 如果更新字体大小变量,则按钮组件中的文本将会改变。然后,实例将需要更新,并且因为它位于自动布局框架中,框架及其内容的布局也将根据实例的新尺寸进行更新。 QueryGraph的依赖边可以发生变化。一个用户对依赖图的更改可能会导致另一个用户产生大量的多人游戏流量。例如,如果一个用户将一个实例换到另一个组件上,多人游戏可能需要意识到他们的合作者现在需要接收该组件及其所有后代(及其依赖项)-即使第一个用户没有触及那些节点。该用户只是使这些节点对他们的合作者变得“可到达”,系统必须相应地做出响应。

验证依赖关系 #

用户在动态加载的文件中执行的操作应该产生与完全加载文件时相同的一组更改。为了实现编辑的一致性,所需的写依赖关系集必须完美无缺。缺少一个依赖关系可能导致客户端无法更新下游节点上的派生数据。对用户而言,这些错误看起来像是严重的错误:实例与其支持的节点发生分歧。 我们想要确保客户严格按照我们列举的写入依赖角色进行节点编辑。为了验证这一点,我们在一个shadow模式下运行了多人游戏一段时间。在这种模式下,多人游戏会跟踪用户所在的页面,计算写入依赖关系,就好像它们是动态加载的,而实际上并没有改变任何运行时行为。如果多人游戏接收到任何超出写入依赖集之外的节点编辑,它会报告错误。

使用这个验证框架,我们成功地识别了我们在最初实现中遗漏的依赖关系。例如,我们发现了一个涉及帧约束和实例的复杂的跨页面递归写入依赖关系。如果我们没有正确处理这个依赖关系,编辑可能会导致布局计算不完整。通过我们的阴影验证框架,我们能够识别出 在为编辑器动态加载之前,客户端可以直接下载一个经过编码的 Figma 文件,而无需我们的多人游戏系统在内存中解码它。但是,随着动态页面加载,服务器需要首先解码 Figma 文件并在内存中构建 QueryGraph,以确定要发送给客户端的内容。这个解码过程可能耗时并且处于关键路径上,因此优化至关重要。

首先,我们确保多人游戏可以尽早开始解码过程。一旦 Figma 收到页面加载的初始 GET 请求,我们的后端向多人游戏发送一个提示,指示文件加载即将发生,并且应该开始预加载文件。这样,多人游戏甚至在客户端与其建立 WebSocket 连接之前就开始下载和解码文件。预加载是一种优化方法,可以填补任何差距,引入额外的测试,并更新 QueryGraph 以避免类似的错误。 以这种方式处理可以从75%的负载时间中减少300-500毫秒。

接下来,我们引入了并行解码,这是一种优化,我们将原始偏移持久化在Figma文件中,从而使我们能够将解码工作分成多个CPU核心可以同时处理的块。串行解码二进制编码的Figma文件可能会相当慢(对于我们最大的文件超过五秒!),因此,实际上,这会将解码时间减少超过40%。

减少多人游戏系统发送给客户端的数据量对于动态页面加载是一个重要的优势,但我们意识到我们可以通过额外的客户端优化进一步改进。具体来说,客户端会在内存中缓存实例节点的后代,以便用户可以轻松编辑和交互。但实例“子层”完全可以从实例的支持组件和用户设置的任何覆盖派生,因此在初始文件加载时没有必要将它们全部实例化。作为动态页面加载的一部分, 我们现在推迟在其他页面上的节点上实例化子层。 这带来了巨大的加载时间优势,但需要更新数十个子系统,以消除所有节点在加载时都完全实例化的假设,并支持懒惰,延迟的实例化。

我们在六个月的时间内将动态页面加载发送给用户群,仔细测量我们的更改对受控A/B测试的影响,并监视我们的自动遥测。 最终,我们看到了一些很好的结果:

  • 尽管文件每年增加18%,但最慢和最复杂的文件加载速度提高了33%
  • 通过仅加载用户需要的内容,客户端内存中的节点数量减少了70%
  • 出现内存不足错误的用户减少了33%

我们始终在寻找优化加载时间和减少内存的机会,随着文件变得越来越大和更加复杂,动态页面加载变得愈发重要。 在性能改进中,异步加载已经成为我们的基础。如果您对这类工作感兴趣,请查看我们的招聘职位- 我们正在招聘 (opens new window)

感谢所有参与其中的人,包括Andrew Chan,Andrew Swan,Darren Tsung,Eddie Shiang,Georgia Rust,Jackie Chui,John Lai,Melissa Kwan和Raghav Anand。

插图由María Medem制作

订阅Figma的编辑通讯 #

我同意订阅Figma的邮件列表。

相关文章 #

使用Figma创建和协作 #

免费开始 (opens new window)