Relay 对应用程序创作的方案,使最佳运行时性能和应用程序可维护性独特地结合在一起。在这篇文章中,我将描述大多数应用程序在数据获取方面不得不做出的权衡,然后描述 Relay 的方案如何使你能够绕过这些权衡,并在多个权衡维度上获得最佳结果。
在基于组件的 UI 系统(如 React)中,一个重要的决策是在 UI 树的哪个位置获取数据。虽然数据获取可以在 UI 树中的任何位置完成,但为了理解所涉及的权衡,让我们考虑两个极端。
- 叶子节点:直接在每个使用数据的组件内获取数据
- 根节点:在 UI 的根部获取所有数据,并通过属性传递将其传递到叶子节点。
你在 UI 树的哪个位置获取数据会影响应用程序的性能和可维护性的多个方面。不幸的是,对于朴素的数据获取,没有一个极端在所有方面都是最佳的。让我们看看这些方面,并考虑哪些方面随着你将数据获取移动到叶子节点而改善,哪些方面随着你将数据获取移动到根节点而改善。
加载体验
- 🚫 叶子节点:如果各个节点获取数据,你将最终得到请求级联,其中你的 UI 需要按顺序(瀑布式)进行多个请求往返,因为 UI 的每一层都阻塞在父层的渲染上。此外,如果多个组件碰巧使用相同的数据,你最终将多次获取相同的数据。
- ✅ 根节点:如果所有数据都在根部获取,你将进行一次请求,并在没有任何重复数据或级联请求的情况下渲染整个 UI。
Suspense 级联
- 🚫 叶子节点:如果每个组件都需要单独获取数据,每个组件将在初始渲染时挂起。使用 React 的当前实现,取消挂起会导致从最近的父级 Suspense 边界重新渲染。这意味着你必须在初始加载期间 O(n) 次重新评估产品组件代码,其中 n 是树的深度。
- ✅ 根节点:如果所有数据都在根部获取,你将只挂起一次,并且只评估一次产品组件代码。
可组合性
- ✅ 叶子节点:在一个新地方使用现有组件就像渲染它一样简单。删除一个组件就像不渲染它一样简单。类似地,添加/删除数据依赖关系可以完全在本地完成。
- 🚫 根节点:将现有组件作为另一个组件的子组件添加,需要更新包含该组件的每个查询,以获取新数据,然后将新数据传递到所有中间层。类似地,删除一个组件需要追踪这些数据依赖关系,回到每个根组件,并确定你删除的组件是否是该数据的最后一个消费者。同样的动态适用于向现有组件添加/删除新数据。
细粒度更新
- ✅ 叶子节点:当数据发生变化时,每个读取该数据的组件都可以单独重新渲染,避免重新渲染未受影响的组件。
- 🚫 根节点:由于所有数据都起源于根部,因此当任何数据更新时,它始终强制根组件更新,从而强制对整个组件树进行昂贵的重新渲染。
Relay
Relay 利用 GraphQL 片段和编译器构建步骤,提供更优化的替代方案。在一个使用 Relay 的应用程序中,每个组件都定义了一个 GraphQL 片段,该片段声明它需要的数据。这包括组件将渲染的具体值,以及它将渲染的每个直接子组件的片段(按名称引用)。
在构建时,Relay 编译器会收集这些片段,并为应用程序中的每个根节点构建一个单独的查询。让我们看看这种方法是如何针对上面描述的每个维度进行的。
- ✅ 加载体验 - 编译器生成的查询在一个往返中获取表面所需的所有数据。
- ✅ Suspense 级联 - 由于所有数据都是在一个请求中获取的,我们只挂起一次,并且就在树的根部。
- ✅ 可组合性 - 从组件中添加/删除数据(包括渲染子组件所需的片段数据)可以在单个组件内本地完成。编译器负责更新所有受影响的根查询。
- ✅ 细粒度更新 - 由于每个组件都定义了一个片段,Relay 准确地知道每个组件消耗了哪些数据。这使 Relay 能够执行最佳更新,其中当数据发生变化时,只重新渲染最小的组件集。
总结
如你所见,Relay 使用声明性可组合数据获取语言(GraphQL)以及编译器步骤,使我们能够在上面概述的所有权衡维度上实现最佳结果。
叶子节点 | 根节点 | GraphQL/Relay | |
---|---|---|---|
加载体验 | 🚫 | ✅ | ✅ |
Suspense 级联 | 🚫 | ✅ | ✅ |
可组合性 | ✅ | 🚫 | ✅ |
细粒度更新 | ✅ | 🚫 | ✅ |