relay-performance

Mejores prácticas de rendimiento para aplicaciones Relay. Úselo al optimizar la obtención de datos, reducir los re-renderizados, configurar el almacenamiento en caché o mejorar el tiempo hasta el primer…

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.

Más skills de facebook

gc-safe-coding
facebook
Para la explicación completa y el fundamento, consulta doc/GCSafeCoding.md.
app-review-prep
facebook
Prepara una app de Meta para App Review: comprueba el estado actual, los requisitos pendientes, los privilegios concedidos y el historial de envíos. Úsalo antes de enviar una app…
api-health
facebook
Monitorea la salud de la API para una app de Meta: verifica límites de tasa, volumen de llamadas y deprecaciones de API. Úsalo para diagnosticar limitaciones, planificar capacidad o prepararse para versiones de API…
debug-webhooks
facebook
Soluciona problemas de webhooks para una aplicación de Meta — inspecciona las suscripciones activas, identifica configuraciones incorrectas y envía cargas de prueba para verificar la entrega. Usa cuando…
api-integration
facebook
Guía a un desarrollador para configurar una integración de la API de Meta desde cero: descubre las APIs adecuadas, obtiene guías de configuración, requisitos de autenticación,…
webhook-setup
facebook
Configura webhooks para una app de Meta de principio a fin: descubre los temas disponibles, suscríbete a campos y verifica con una carga de prueba. Úsalo al configurar webhooks para…
test-ui
facebook
Prueba el sistema de UI (PanelUI, ScreenSpace) contra el ejemplo de poke usando la CLI de iwsdk.
flags
facebook
We need to translate the given text from English to Spanish. The text describes a skill for inspecting and comparing feature flag states across React release channels. We must preserve product names, protocol names, URLs, numbers, and technical terms. The name "flags" is not in the text, so we don't include it. We translate only the text inside <text>. No extra commentary, labels, etc. The text: "Inspect and compare feature flag states across React release channels. View all flags across channels (www, www-modern, canary, next, experimental, rn variants) or compare specific channels with --diff Output formats include default table view, CSV export, and cleanup status grouping Flag states indicated by symbols: enabled (✅), disabled (❌), variant testing (🧪), profiling-only (📊) Common pitfall: __VARIANT__ flags are tested in both states on www; use --diff to spot meaningful..." We need to translate to Spanish. Keep technical terms like "React", "www", "www-modern", "canary",