React QueryでフロントエンドのPerformanceを改善する実践ガイド
対象読者
- React Queryの基本を理解している方
- フロントエンドのパフォーマンス改善に興味がある方
所要時間: 約12分
はじめに
React Queryを「データ取得ライブラリ」として使っている方は多いと思います。
しかし、適切に設定するだけでフロントエンドのパフォーマンスは大きく向上します。
本記事では、「なんとなく動いている状態」から計測・改善・維持できる状態に引き上げるための実践的な方法を紹介します。
なぜReact QueryはPerformanceに効くのか
React Queryがパフォーマンスに寄与する理由は主に3つです。
1. キャッシュによる重複リクエストの排除
同じ queryKey のクエリは1回しかフェッチされません。
例:10コンポーネントが同じデータを要求 → APIコールは1回だけ
2. バックグラウンド再フェッチによるUX向上
- キャッシュを即表示
- 裏で最新データを取得
- ローディング時間を大幅に削減
3. 不要な再レンダリングの抑制
変更があった部分のみ再レンダリングされます。select を使うとさらに細かく制御可能です。
改善① — staleTimeを適切に設定する
デフォルトの問題点
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
// staleTime: 0(デフォルト)
});
- データ取得直後に「stale」扱い
- タブ切り替えごとに再フェッチ
- 無駄なリクエストが発生
データに応じた設定
// ほぼ変わらないデータ
useQuery({
queryKey: ['categories'],
queryFn: fetchCategories,
staleTime: 1000 * 60 * 60, // 1時間
});
// 数分ごとに更新
useQuery({
queryKey: ['dashboard'],
queryFn: fetchDashboard,
staleTime: 1000 * 60 * 5, // 5分
});
// リアルタイムデータ
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
staleTime: 0,
refetchInterval: 1000 * 30,
});
グローバル設定
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
gcTime: 1000 * 60 * 10,
retry: 1,
refetchOnWindowFocus: false,
},
},
});
refetchOnWindowFocus: false は多くのアプリで有効です。
改善② — selectで再レンダリングを最適化
問題
function UserIdList() {
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const ids = data?.map(u => u.id);
return <div>{ids?.join(', ')}</div>;
}
→ 不要な再レンダリングが発生します。
解決
function UserIdList() {
const { data: ids } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.map(u => u.id),
});
return <div>{ids?.join(', ')}</div>;
}
select を使うことで必要なデータのみを購読できます。
特定データのみ取得
function useUserName(userId: number) {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.find(u => u.id === userId)?.name,
});
}
改善③ — Prefetchingで体感速度を向上
ホバー時にPrefetch
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ['posts', post.id],
queryFn: () => fetchPost(post.id),
staleTime: 1000 * 60,
});
}}
クリック時にはデータが取得済みになります。
ページネーション先読み
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => fetchPosts(page + 1),
});
}, [page]);
ページ遷移がスムーズになります。
改善④ — initialDataでローディング削減
initialData: () => {
const posts = queryClient.getQueryData(['posts']);
return posts?.find(p => p.id === postId);
},
リストから詳細への遷移でローディングを省略できます。
isLoading と isFetching の違い
| 状態 | 意味 |
|---|---|
isLoading |
データが存在しない |
isFetching |
バックグラウンドで通信中 |
改善⑤ — 並列クエリで高速化
自動並列
const { data: users } = useQuery(...)
const { data: posts } = useQuery(...)
const { data: stats } = useQuery(...)
自動で並列実行されます。
動的並列
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
})),
});
改善⑥ — DevToolsで可視化
npm install @tanstack/react-query-devtools
<ReactQueryDevtools initialIsOpen={false} />
確認できる内容:
- キャッシュの中身
- クエリ状態(fresh / stale / fetching)
- フェッチ回数
- GCタイミング
改善は必ず実測で確認することが重要です。
まとめ — 優先順位をつける
| 優先度 | 改善項目 | 効果 | 難易度 |
|---|---|---|---|
| 高 | staleTime |
無駄なリクエスト削減 | 低 |
| 高 | refetchOnWindowFocus: false |
不要なフェッチ防止 | 低 |
| 中 | select |
再レンダリング削減 | 中 |
| 中 | Prefetching | 体感速度向上 | 中 |
| 中 | 並列クエリ | 待ち時間短縮 | 低 |
| 低 | initialData |
ローディング削減 | 高 |
まずは以下から取り組むのがおすすめです:
-
staleTimeの設定 -
refetchOnWindowFocus: falseの設定
参考リンク
この記事が役に立ったら LGTM をお願いします 🙏
質問・指摘はコメントで気軽にどうぞ!