relay-performance

bởi facebook

Các phương pháp thực hành tốt nhất về hiệu suất cho ứng dụng Relay. Sử dụng khi tối ưu hóa việc tìm nạp dữ liệu, giảm render lại, cấu hình bộ nhớ đệm hoặc cải thiện thời gian đến đầu tiên…

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.

Thêm skills từ facebook

gc-safe-coding
facebook
Để biết giải thích đầy đủ và lý do, hãy xem doc/GCSafeCoding.md.
app-review-prep
facebook
Chuẩn bị ứng dụng Meta cho App Review — kiểm tra trạng thái hiện tại, các yêu cầu còn tồn đọng, quyền đã được cấp và lịch sử gửi duyệt. Sử dụng trước khi gửi ứng dụng…
api-health
facebook
Theo dõi tình trạng API cho ứng dụng Meta — kiểm tra giới hạn tỷ lệ, khối lượng cuộc gọi và các API bị ngừng hỗ trợ. Dùng để chẩn đoán tình trạng hạn chế, lập kế hoạch dung lượng hoặc chuẩn bị cho phiên bản API…
debug-webhooks
facebook
Khắc phục sự cố webhook cho ứng dụng Meta — kiểm tra các đăng ký đang hoạt động, xác định cấu hình sai và gửi payload thử nghiệm để xác minh việc phân phối. Sử dụng khi…
api-integration
facebook
Hướng dẫn nhà phát triển thiết lập tích hợp Meta API từ đầu — khám phá các API phù hợp, tìm nạp hướng dẫn thiết lập, yêu cầu xác thực,…
webhook-setup
facebook
Thiết lập webhooks cho ứng dụng Meta từ đầu đến cuối — khám phá các chủ đề có sẵn, đăng ký nhận các trường và xác minh bằng payload kiểm thử. Sử dụng khi cấu hình webhooks cho…
test-ui
facebook
Kiểm thử hệ thống UI (PanelUI, ScreenSpace) với ví dụ poke bằng iwsdk CLI.
flags
facebook
Kiểm tra và so sánh trạng thái cờ tính năng trên các kênh phát hành React. Xem tất cả cờ trên các kênh (www, www-modern, canary, next, experimental, biến thể rn) hoặc so sánh các kênh cụ thể với --diff. Định dạng đầu ra bao gồm chế độ xem bảng mặc định, xuất CSV và nhóm trạng thái dọn dẹp. Trạng thái cờ được biểu thị bằng ký hiệu: bật (✅), tắt (❌), kiểm thử biến thể (🧪), chỉ lập hồ sơ (📊). Lỗi thường gặp: cờ __VARIANT__ được kiểm thử ở cả hai trạng thái trên www; sử dụng --diff để phát hiện sự khác biệt có ý nghĩa...