mcpField
R
relay-performance

relay-performance

github.com/facebook开发19.0k49 次安装今天更新
代码仓库

关于 relay-performance

relay-performance 是面向 Relay 应用开发者的性能优化指导技能,提供查询、缓存、分页和片段设计建议。它重点说明如何减少请求负担、缩短关键渲染路径并避免无界数据获取。

主要功能

  1. 01每个页面使用少量根查询
  2. 02在渲染前预加载数据
  3. 03用 `@defer` 延迟非关键内容
  4. 04按需选择获取策略
  5. 05为集合数据配置分页

适用场景

  1. 01减少单个页面的重复请求
  2. 02加快首屏关键内容呈现
  3. 03控制长列表的数据传输量
  4. 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 规则会捕获此类问题。

如果子组件需要更多数据,就在子组件中添加片段,并在父组件中展开它——不要扩大父组件的片段。

评论

支持 Markdown