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 앱의 웹훅 문제를 해결합니다 — 활성 구독을 검사하고, 잘못된 구성을 식별하며, 테스트 페이로드를 전송하여 전달을 확인합니다. 다음과 같은 경우에 사용하세요…
api-integration
facebook
개발자가 Meta API 통합을 처음부터 설정하도록 안내합니다 — 적절한 API를 찾아내고, 설정 가이드, 인증 요구 사항 등을 가져옵니다…
webhook-setup
facebook
Meta 앱용 웹훅을 처음부터 끝까지 설정하세요 — 사용 가능한 주제를 탐색하고, 필드를 구독하고, 테스트 페이로드로 검증합니다. 웹훅을 구성할 때 사용하세요…
test-ui
facebook
iwsdk CLI를 사용하여 포크 예제에 대해 Test UI 시스템(PanelUI, ScreenSpace)을 테스트합니다.
flags
facebook
React 릴리스 채널 간 기능 플래그 상태를 검사하고 비교합니다. 모든 채널(www, www-modern, canary, next, experimental, rn 변형)의 플래그를 보거나 --diff로 특정 채널을 비교합니다. 출력 형식은 기본 테이블 보기, CSV 내보내기, 정리 상태 그룹화를 포함합니다. 플래그 상태는 기호로 표시됩니다: 활성화(✅), 비활성화(❌), 변형 테스트(🧪), 프로파일링 전용(📊). 일반적인 실수: __VARIANT__ 플래그는 www에서 두 상태 모두 테스트되며, --diff를 사용하여 의미 있는 차이를 찾습니다...