relay-performance について
relay-performanceは、Relayアプリ開発者向けのパフォーマンス最適化ガイダンススキルです。クエリ、キャッシュ、ページネーション、フラグメント設計を扱い、リクエスト負荷や重要な描画経路を改善し、無制限のデータ取得を避ける方法を示します。
主な機能
- 01画面ごとのルートクエリを少数にする
- 02描画前のデータプリロード
- 03`@defer`による非重要コンテンツの遅延
- 04フェッチポリシーの適切な選択
- 05コレクションデータのページネーション
活用シーン
- 01画面内の重複リクエスト削減
- 02重要コンテンツの早期描画
- 03長いリストのデータ転送量の抑制
- 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/ で利用できます)。
画面ごとにクエリを 1 つにする
各画面またはルートでは、ルートクエリを1 つ(またはごく少数)にしてください。Relay はすべてのフラグメントのデータ要件を、クエリごとに 1 つのネットワークリクエストにまとめます。同じ画面で複数のルートクエリを使うと、この最適化が損なわれます。ブラウザーは、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 | オフライン優先の場合、またはストア内にあることが保証されたデータを読み取る場合。 |
network-only はまれなケースに限って使用してください。使いすぎると Relay がキャッシュなしのクライアントになり、正規化ストアの利点が失われます。
ガベージコレクションを設定する
Relay Store の gcReleaseBufferSize を設定し、コンポーネントのアンマウント後も最近使用されたクエリを保持します。デフォルト値は 10 です。これにより、以前訪問した画面に戻る際、新たなネットワークリクエストを発生させず、すぐに表示できます(データがストアに残っているためです)。
const store = new Store(new RecordSource(), {
gcReleaseBufferSize: 20,
});
画面が多いアプリや画面間の移動が多いアプリでは、バッファーを増やしてください。メモリに制約のある環境(モバイル)では、控えめな値にしてください。
サーバー側でフィルターと並べ替えを行う
データをすべて取得して JavaScript で処理するのではなく、GraphQL フィールド引数を使ってサーバー側でデータをフィルターし、並べ替えてください。
// 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 を使い、リストフィールドには必ずページネーションを適用してください。コレクション全体を一度に取得すると、数メガバイトのデータ転送や、正規化中の 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 が再レンダリングできるようにしてください。多くのコンポーネントで 1 つの巨大なフラグメントを共有すると、そのフラグメント内のフィールドが変更された際に、すべてのコンポーネントが再レンダリングされます。
// 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
}
コンポーネントごとにコネクションを 1 つにする
コンポーネントごとに usePaginationFragment を 1 つだけ使ってください。1 つのコンポーネントに複数のコネクションを持たせると、ページネーションの状態を把握しにくくなります。カーソルの追跡、読み込み状態、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 ルールで検出できます。
子コンポーネントにさらにデータが必要な場合は、子コンポーネントにフラグメントを追加し、 親で展開してください。親のフラグメントを広げないでください。