relay-performance

作者: facebook

Relay 應用程式的效能最佳實務。適用於最佳化資料擷取、減少重新渲染、設定快取,或改善首次… 時間。

npx skills add https://github.com/facebook/relay --skill relay-performance

Relay Performance Best Practices

Performance-focused guidance for Relay applications. For correctness, naming, and architectural patterns, see the relay-best-practices skill.

For detailed API documentation, read the relevant page from <llm-docs>/ (available in node_modules/relay-runtime/llm-docs/ after v20.1.1).

One Query Per Screen

Each screen or route should have one (or very few) root queries. Relay coalesces all fragment data needs into a single network request per query. Multiple root queries on the same screen defeat this optimization — the browser makes multiple parallel requests that each carry redundant overhead (HTTP headers, connection setup, response parsing).

GOOD:                              BAD:
Route → 1 query                    Route → 3 queries
  ├─ Header (fragment)               ├─ Header (query #1)
  ├─ Content (fragment)               ├─ Content (query #2)
  └─ Sidebar (fragment)               └─ Sidebar (query #3)

Preload Before Rendering the Root

Fetch the initial query before calling createRoot().render(). This overlaps the network request with React's initialization, minimizing time to first meaningful paint.

// 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>
);

Use @defer for Non-Critical Content

Defer secondary or below-the-fold content so primary UI renders faster. Relay streams deferred data progressively via Suspense — the initial response arrives smaller and the critical path renders sooner.

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>
  );
}

Good candidates for @defer:

  • Sidebar content
  • Below-the-fold sections
  • Tabs and accordions not visible on initial load
  • Heavy item details in paginated lists

Fetch Policies

store-or-network (the default) is correct for most cases — it reuses cached data and only hits the network for missing or stale data.

PolicyWhen to use
store-or-networkDefault. Best balance of speed and freshness.
store-and-networkShow cached data immediately, update in background.
network-onlyFreshness is critical (e.g., after a mutation with wide side effects).
store-onlyOffline-first or reading data already guaranteed to be in the store.

Reserve network-only for rare cases. Overusing it turns Relay into a no-cache client and eliminates the benefit of the normalized store.

Configure Garbage Collection

Set gcReleaseBufferSize on the Relay Store to retain recently-used queries after their components unmount. The default is 10. This makes navigating back to a previously visited screen instant (data is still in the store) instead of triggering a new network request.

const store = new Store(new RecordSource(), {
  gcReleaseBufferSize: 20,
});

For apps with many screens or heavy navigation, increase the buffer. For memory-constrained environments (mobile), keep it conservative.

Filter and Sort on the Server

Use GraphQL field arguments to filter and sort data on the server rather than fetching everything and processing in 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);

Server-side filtering reduces payload size, avoids unnecessary network transfer, and reduces memory usage on the client.

Never Fetch Unbounded Collections

Always paginate list fields using @connection + usePaginationFragment. Fetching an entire collection at once risks transferring megabytes of data, stalling the UI during normalization, and exhausting device memory.

Start with a page size appropriate for the viewport (e.g., 10–20 items) and load more on scroll.

# 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
      }
    }
  }
}

Keep Fragments Granular

Split large fragments into smaller, component-scoped fragments so Relay can re-render only the components whose data actually changed. A single monolithic fragment shared by many components causes all of them to re-render when any field in the fragment changes.

// 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
}

One Connection Per Component

Use a single usePaginationFragment per component. Multiple connections in one component make pagination state harder to reason about — cursor tracking, loading states, and hasNext flags become tangled. Split each connection into its own component instead.

Avoid Unnecessary Refetches

After a mutation, let Relay's normalized store auto-update components by spreading relevant fragments in the mutation response. Do not call refetch() or fetchQuery() when the store update is sufficient.

# 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 }
  }
}

Use fetchKey sparingly — changing it forces a full network round trip. Reserve refetchQueries / manual refetch for cases where the mutation's side effects are too broad to capture in the response payload.

Fetch Only What You Need

Each fragment should request only the fields the component actually renders. Do not add fields "just in case" — unused fields increase payload size and slow down parsing and normalization. Relay's unused-fields lint rule catches this.

If a child component needs more data, add a fragment to the child and spread it in the parent — do not widen the parent's fragment.

來自 facebook 的更多技能

gc-safe-coding
facebook
完整解釋與理由請參閱 doc/GCSafeCoding.md。
app-review-prep
facebook
準備將 Meta 應用程式提交至 App Review——檢查目前狀態、未完成的要求、已授予的權限及提交紀錄。請在提交應用程式前使用……
api-health
facebook
監控 Meta 應用程式的 API 健康狀態 — 檢查速率限制、呼叫量與 API 棄用情況。用於診斷節流問題、規劃容量,或為 API 版本…做好準備。
debug-webhooks
facebook
排解 Meta 應用程式的 webhook 問題——檢查作用中的訂閱、找出設定錯誤,並傳送測試負載以驗證傳遞。當……時使用。
api-integration
facebook
引導開發者從零開始設定 Meta API 整合 — 找出正確的 API、取得設定指南、驗證需求、…
webhook-setup
facebook
端到端設定 Meta 應用程式的 Webhook — 探索可用主題、訂閱欄位,並透過測試負載驗證。在為… 設定 Webhook 時使用。
test-ui
facebook
使用 iwsdk CLI 針對 poke 範例測試 UI 系統(PanelUI、ScreenSpace)。
flags
facebook
檢查並比較 React 發佈通道中的功能旗標狀態。檢視所有通道(www、www-modern、canary、next、experimental、rn 變體)的旗標,或使用 --diff 比較特定通道。輸出格式包括預設表格檢視、CSV 匯出及清理狀態分組。旗標狀態以符號表示:啟用(✅)、停用(❌)、變體測試(🧪)、僅分析(📊)。常見陷阱:__VARIANT__ 旗標在 www 上會以兩種狀態進行測試;使用 --diff 可找出有意義的差異。