关于 relay-performance
relay-performance 是面向 Relay 应用开发者的性能优化指导技能,提供查询、缓存、分页和片段设计建议。它重点说明如何减少请求负担、缩短关键渲染路径并避免无界数据获取。
主要功能
- 01每个页面使用少量根查询
- 02在渲染前预加载数据
- 03用 `@defer` 延迟非关键内容
- 04按需选择获取策略
- 05为集合数据配置分页
适用场景
- 01减少单个页面的重复请求
- 02加快首屏关键内容呈现
- 03控制长列表的数据传输量
- 04减少不必要的重新获取
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ facebook/ relay --skill relay-performance安装后在 Claude Code 中输入/relay-performance确认已加载。
详细介绍
Relay 性能最佳实践
面向 Relay 应用的性能优化指南。有关正确性、命名和架构模式,请参阅 relay-best-practices 技能。
有关 API 的详细文档,请阅读 <llm-docs>/ 中的相关页面(v20.1.1 及更高版本安装后位于 node_modules/relay-runtime/llm-docs/)。
每个屏幕使用一个查询
每个屏幕或路由应只有 一个(或极少数几个)根查询。Relay 会将所有片段的数据需求合并到每个查询的单个网络请求中。在同一屏幕上使用多个根查询会破坏这种优化——浏览器会发起多个并行请求,每个请求都会产生重复的开销(HTTP 标头、连接建立、响应解析)。
GOOD: BAD:
Route → 1 query Route → 3 queries
├─ Header (fragment) ├─ Header (query #1)
├─ Content (fragment) ├─ Content (query #2)
└─ Sidebar (fragment) └─ Sidebar (query #3)
渲染根组件前预加载
在调用 createRoot().render() 之前获取初始查询。这样可让网络请求与 React 初始化并行进行,最大限度缩短首次有意义绘制时间。
// Start fetch immediately — before React even initializes
const queryRef = loadQuery(environment, AppQuery, initialVariables);
// Then render — data may already be available
const root = createRoot(document.getElementById('root'));
root.render(
<RelayEnvironmentProvider environment={environment}>
<Suspense fallback={<AppSkeleton />}>
<App queryRef={queryRef} />
</Suspense>
</RelayEnvironmentProvider>
);
使用 @defer 延迟非关键内容
延迟加载次要内容或首屏以下的内容,以便更快渲染主要 UI。Relay 通过 Suspense 逐步传输延迟的数据——初始响应更小,关键路径也能更早完成渲染。
query ProfileScreenQuery($id: ID!) {
user(id: $id) {
...ProfileHeader_user
...ProfileDetails_user @defer
...ProfileComments_user @defer
}
}
function ProfileScreen({ queryRef }) {
const data = usePreloadedQuery(ProfileScreenQuery, queryRef);
return (
<ScrollView>
<ProfileHeader user={data.user} />
<Suspense fallback={<DetailsSkeleton />}>
<ProfileDetails user={data.user} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<ProfileComments user={data.user} />
</Suspense>
</ScrollView>
);
}
适合使用 @defer 的内容:
- 侧边栏内容
- 首屏以下的部分
- 初始加载时不可见的标签页和折叠面板
- 分页列表中的复杂项目详情
获取策略
store-or-network(默认值)适用于大多数情况——它会复用缓存数据,只在数据缺失或过时的情况下访问网络。
| 策略 | 使用场景 |
|---|---|
store-or-network | 默认值。在速度和新鲜度之间取得最佳平衡。 |
store-and-network | 立即显示缓存数据,并在后台更新。 |
network-only | 新鲜度至关重要时(例如,发生具有广泛副作用的变更后)。 |
store-only | 离线优先,或读取已确保存储在 Store 中的数据。 |
仅在少数情况下使用 network-only。过度使用会让 Relay 变成无缓存客户端,并消除规范化 Store 带来的优势。
配置垃圾回收
在 Relay Store 上设置 gcReleaseBufferSize,以便组件卸载后保留最近使用的查询。默认值为 10。这样,返回之前访问过的屏幕时即可立即显示(数据仍在 Store 中),而无需触发新的网络请求。
const store = new Store(new RecordSource(), {
gcReleaseBufferSize: 20,
});
对于屏幕较多或导航频繁的应用,可增大缓冲区。对于内存受限的环境(如移动设备),应采用较保守的设置。
在服务端筛选和排序
使用 GraphQL 字段参数在服务端筛选和排序数据,而不是获取全部数据后再用 JavaScript 处理。
// BAD: fetch all tasks, filter on client
const data = useFragment(graphql`
fragment TaskList_user on User {
tasks { id, title, status }
}
`, user);
const active = data.tasks.filter(t => t.status === 'ACTIVE');
// GOOD: filter on server via field argument
const data = useFragment(graphql`
fragment TaskList_user on User {
tasks(status: ACTIVE) { id, title }
}
`, user);
服务端筛选可减小负载,避免不必要的网络传输,并降低客户端内存使用量。
切勿获取无界集合
始终使用 @connection + usePaginationFragment 对列表字段进行分页。一次性获取整个集合可能会传输数 MB 的数据,导致规范化期间 UI 卡顿,并耗尽设备内存。
从适合视口的页面大小开始(例如,每页 10–20 个项目),并在滚动时加载更多。
# BAD: fetches every item — unbounded
fragment NotificationList_user on User {
notifications {
id
message
}
}
# GOOD: paginated with a bounded first page
fragment NotificationList_user on User
@argumentDefinitions(
count: { type: "Int", defaultValue: 10 }
cursor: { type: "String" }
)
@refetchable(queryName: "NotificationListPaginationQuery") {
notifications(first: $count, after: $cursor)
@connection(key: "NotificationList_notifications") {
edges {
node {
id
message
}
}
}
}
保持片段粒度精细
将大型片段拆分为较小的、作用域限定于组件的片段,让 Relay 仅重新渲染数据实际发生变化的组件。多个组件共享单个庞大的片段时,该片段中的任何字段发生变化都会导致所有组件重新渲染。
// BAD: one large fragment, all children re-render on any field change
function PostCard({ post }) {
const data = useFragment(graphql`
fragment PostCard_post on Post {
title
body
author { name, profilePicture { uri } }
likeCount
commentCount
}
`, post);
return (
<>
<PostHeader title={data.title} author={data.author} />
<PostBody body={data.body} />
<PostFooter likes={data.likeCount} comments={data.commentCount} />
</>
);
}
// GOOD: each child owns its fragment, re-renders independently
function PostHeader({ post }: { post: PostHeader_post$key }) {
const data = useFragment(graphql`
fragment PostHeader_post on Post {
title
author { name }
}
`, post);
// Only re-renders when title or author.name changes
}
function PostFooter({ post }: { post: PostFooter_post$key }) {
const data = useFragment(graphql`
fragment PostFooter_post on Post {
likeCount
commentCount
}
`, post);
// Only re-renders when like/comment counts change
}
每个组件只使用一个连接
每个组件使用一个 usePaginationFragment。在一个组件中使用多个连接会让分页状态更难理清——游标跟踪、加载状态和 hasNext 标记会相互纠缠。应将每个连接拆分到独立组件中。
避免不必要的重新获取
执行变更后,在变更响应中展开相关片段,让 Relay 的规范化存储自动更新组件。只要更新存储就足够,就不要调用 refetch() 或 fetchQuery()。
# GOOD: updated data comes back with the mutation response
mutation UpdateUserMutation($input: UpdateUserInput!) {
updateUser(input: $input) {
user {
...UserProfile_user
...UserAvatar_user
}
}
}
# BAD: requires a separate round-trip after mutation
mutation UpdateUserMutation($input: UpdateUserInput!) {
updateUser(input: $input) {
user { id }
}
}
谨慎使用 fetchKey——更改它会强制进行完整的网络往返。只有当变更的副作用范围太广,无法通过响应负载捕获时,才使用 refetchQueries / 手动重新获取。
仅获取所需数据
每个片段都应仅请求组件实际渲染的字段。不要“以防万一”添加字段——未使用的字段会增加负载大小,并拖慢解析和规范化。Relay 的 unused-fields lint 规则会捕获此类问题。
如果子组件需要更多数据,就在子组件中添加片段,并在父组件中展开它——不要扩大父组件的片段。