はじめに
2026年1月14日にVercel社から、10年以上のReactとNest.jsの最適化ノウハウをまとめたReact Best Practicesが発表されました。
AIエージェントに対して専門的な知識を付与するSkillsも準備してくれており、こちらをAgent Skillsとしてインストールすれば、コーディングやレビューに活用することができます。
特にノウハウの中身(rules)を僕らが理解していなくても
Skillsとして導入すればAIが内容を理解して、最適なタイミングでルールを適用してくれます。
ただプロジェクトに導入する場合、LLMのコンテキストも限られている中、最低限でも内容を理解して「必要なものだけ取り入れる」ということもしていくべきかということで、今回はカテゴリー毎に1つずつrulesをざっくり見ていきたいと思います。
実際のrulesはこちらから1つずつ確認できます。
プロジェクト導入のお役に立てましたら幸いです。
全体の構成
React Best Practicesは、全体の構成として以下のようなカテゴリー分けができます。
カテゴリーごとにVercel社が定義した「パフォーマンスへの影響度合い」に基づいて「優先度(CRITICAL〜LOW)」が決められています。
React Best Practices カテゴリ一覧
| 優先度 | カテゴリ | 期待できる効果 |
|---|---|---|
| CRITICAL | 🌊 ウォーターフォールの解消 | LCPの劇的向上、読み込み待機の解消 |
| CRITICAL | 📦 バンドルサイズの最適化 | 初期ロード高速化、JS実行コストの削減 |
| HIGH | 🖥️ サーバーサイドの最適化 | 通信量削減、サーバー負荷の効率化 |
| MEDIUM-HIGH | 🔄 クライアントサイドのデータ取得 | UXの向上、ステート管理の堅牢化 |
| MEDIUM | ⚡ 再レンダリングの最適化 | UIの滑らかさ、CPU負荷の軽減 |
| MEDIUM | 🎨 レンダリングのパフォーマンス | カクつきの解消、大規模データへの対応 |
| LOW-MEDIUM | 🚀 高度なパターン | 保守性・安全性の向上、ロジックの再利用 |
では影響度が最も大きいCLITICALのカテゴリーから順番に確認をしていきましょう!
🌊 ウォーターフォールの解消 優先度:CLITICAL
ここでいうウォーターフォールは「データ取得が直列に連鎖してしまう」ことを指します。
この数珠繋ぎの処理を「並列化」させるのがこのカテゴリーの主目的です。
1. データの必要性が確定するまでawaitを実行しないようにする
関数内で条件分岐(早期リターン)がある場合、その前に重い処理を await してしまうと、結果的に使わないデータの取得を待つことになり、時間が無駄になってしまいます。
// ❌ 良くない例: 権限チェック前に待機が発生
async function getDashboard(user) {
const data = await fetchExpensiveData(); // ここで無条件に待たされる
if (!user.isAdmin) return null;
return data;
}
// ✅ 良い例: 権限を確認してから待機
async function getDashboard(user) {
if (!user.isAdmin) return null;
const data = await fetchExpensiveData(); // 必要な時だけ待つ
return data;
}
2. 独立した非同期処理は先に開始しておいて、必要になる時までawaitしない
「Aの結果を使ってBを叩くが、Cは独立している」場合、Aの完了を待たずにCを開始することで、Cの取得時間を他の処理の裏側に隠すことができます。
// ✅ 良い例: 独立した処理を先に走らせる
async function loadPageData() {
const promiseC = fetchC(); // 独立しているので即座に開始
const dataA = await fetchA();
const dataB = await fetchB(dataA); // Aの結果が必要(直列)
const dataC = await promiseC; // 最後にまとめて受け取る
return { dataB, dataC };
}
3. APIハンドラ内でも「早く開始し、遅く待つ」を徹底する
サーバーサイド(Next.js API Routesなど)の処理でも、外部APIやDBへのリクエストを関数の冒頭で変数に格納し、他の計算処理の後に await することで、通信と計算をオーバーラップすることができます。
// ✅ 良い例: 「前の結果を必要としないもの」を先に走らせておくことで、通信時間をオーバーラップ
// app/api/dashboard/route.ts
export async function GET() {
// 1. 他に依存しないリクエストを「先に開始」しておく(awaitしない)
const settingsPromise = fetchSettings();
// 2. 依存関係がある処理を進める
const user = await fetchUser(); // (500ms)
const posts = await fetchPosts(user.id); // (500ms)
// 3. 最後に、先に走らせておいた結果を待つ
// すでに裏で取得が終わっていれば、待機時間は 0ms になる
const settings = await settingsPromise;
return Response.json({ user, posts, settings });
}
// 合計:約1,000ms で済む(500msの短縮!)
4. 非同期処理をPromise.allで一括実行する
2つのリクエストを1つずつ await すると時間が「和(A+B)」になりますが、Promise.all なら「最大値(max(A, B))」の時間で済ますことができます。
// ❌ 良くない例(1秒 + 1秒 = 2秒かかる)
const user = await getUser();
const settings = await getSettings();
// ✅ 良い例(約1秒で完了)
const [user, settings] = await Promise.all([
getUser(),
getSettings()
]);
5. 重いコンポーネントを<Suspense>で囲み、部分的にレンダリングする
特定のパーツのデータ取得が遅い場合、ページ全体を「待ち状態」にせず、そのパーツだけを fallback(スケルトンやローディングなど)にすることで、ユーザーは他の部分を即座に見たり操作したりできます。
❌ 良くない例(ページ全体がブロックされる)
// app/dashboard/page.tsx
export default async function DashboardPage() {
// 1. 軽いデータ (100ms)
const user = await fetchUser();
// 2. めちゃくちゃ重い統計データ (3秒)
// ❌ これが終わるまで、ユーザーは上の「User情報」すら見ることができない
const stats = await fetchHeavyStats();
return (
<main>
<header>こんにちは、{user.name}さん</header>
<section>
<StatsDisplay data={stats} />
</section>
</main>
);
}
✅ 良い例(ストリーミングによる段階的表示)
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default async function DashboardPage() {
const user = await fetchUser(); // これは早いので待ってもOK
return (
<main>
<header>こんにちは、{user.name}さん</header>
{/* ✅ 重い部分だけを Suspense で囲む */}
<Suspense fallback={<StatsSkeleton />}>
<HeavyStatsSection />
</Suspense>
</main>
);
}
// 別ファイルに切り出した重いコンポーネント
async function HeavyStatsSection() {
const stats = await fetchHeavyStats(); // ここで3秒待つ
return <StatsDisplay data={stats} />;
}
📦 バンドルサイズの最適化 優先度:CLITICAL
主にブラウザが読み込むJavaScriptの量を減らすことを主目的としているカテゴリーです。
6. index.ts から複数のモジュールを一括エクスポートしないようにする
バレルファイル経由で1つだけインポートしても、ファイル内の他の不要な依存関係まで読み込んでしまい、バンドルサイズが膨らむ原因になります。
// ❌ 良くない例 (components/index.ts を経由)
import { Button } from "@/components";
// ✅ 良い例 (直接ファイルを指定)
import { Button } from "@/components/Button";
7. 重いコンポーネントには動的インポートを使用する
グラフ描画ライブラリや複雑なモーダルなどは、ユーザーがそれを必要とした瞬間(ボタンクリックなど)に読み込ませることで、初動の読み込みを軽くできます。
// ✅ 良い例 「React.lazy」で定義したコンポーネントを、 <Suspense> でレンダリングする
import React, { useState, Suspense, lazy } from 'react';
// ✅ 1. React.lazy でインポート。この時点では中身は読み込まれない
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function App() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>マイ・ダッシュボード</h1>
<button onClick={() => setShowChart(true)}>チャートを表示</button>
{showChart && (
// ✅ 2. 読み込みが完了するまでの「待機状態」を Suspense でハンドルする
<Suspense fallback={<div>チャートを読み込んでいます...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
8. ライブラリ全体をインポートせず、必要な関数だけをサブパスから取り出す
lodash などの巨大なライブラリは、名前付きインポート({ debounce })を使っても、設定次第で全体がバンドルに含まれるリスクがあります。直接パスを指定するのが最も安全です。
// ❌ 良くない例
import { debounce } from 'lodash';
// ✅ 良い例
import debounce from 'lodash/debounce';
9. use client を指定するコンポーネント内での巨大なライブラリのインポートを避ける(Next専用)
重い計算やフォーマット処理を「サーバーコンポーネント」側で済ませて、結果の「文字列」や「数値」だけを渡すことで、ブラウザ側の負担を減らすことができます。
// ✅ 良い例:サーバー側で計算を終わらせ、結果だけを渡す
// (Server Component)
import { format } from 'date-fns';
import { ja } from 'date-fns/locale';
export default async function Page() {
const date = new Date();
// サーバー側で文字列に変換(date-fnsはブラウザには送られない)
const formattedDate = format(date, 'yyyy年MM月dd日', { locale: ja });
return <SimpleDateClientDisplay dateString={formattedDate} />;
}
// (Client Component)
'use client';
export function SimpleDateClientDisplay({ dateString }: { dateString: string }) {
// 既にフォーマット済みなので、ライブラリ不要で表示するだけ
return <div>{dateString}</div>;
}
10. 設定ファイルを通じて、特定のライブラリから必要な分だけを効率よく取り出す
lucide-react(アイコン)や MUI のようなライブラリは、数千のコンポーネントを含んでいます。普通にインポートすると、使っていないものまでバンドルに含まれてしまうことがあります。
Next.jsの設定(optimizePackageImports)を使うと、自動的にインポートパスを最適化し、最小限のコードだけを抽出できます。
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// ここに指定したライブラリは、自動的に「必要な分だけ」に最適化される
optimizePackageImports: [
'lucide-react',
'@mui/material',
'@mui/icons-material',
'@headlessui/react'
],
},
};
module.exports = nextConfig;
11. メインスレッド(UIを動かすスレッド)を止めないように、Web Workerを活用する
数万件のデータのソートや複雑な画像解析などをメインスレッドで行うと、その間ブラウザはフリーズし、ボタンも押せなくなります。
Web Workerを使えば、裏側で計算させつつ、UIは滑らかに動かし続けることができます。
🖥️ サーバーサイド最適化 優先度:HIGH
サーバー側で処理を完結させることは、ブラウザの負担を減らす最強の最適化です。
基本的にはこちらのカテゴリーは、主にNext.jsなどのSSRを前提としています。
12. サーバーからクライアントコンポーネントに渡すプロパティ(Props)を最小限にする
サーバーコンポーネントから use client の子要素にオブジェクトを渡すと、ReactはそのオブジェクトをJSON形式に変換(シリアライズ)してHTMLに埋め込みます。
巨大なユーザーオブジェクト全体を渡すと、表示には使わないデータまでブラウザに送られ、通信量が増大します。
// ❌ 良くない例:ユーザーオブジェクト全体(50フィールドくらいあるとする)を渡す
// (Server Component)
const user = await db.user.findUnique({ id: 1 });
return <UserAvatar user={user} />; // 使わないデータまでシリアライズされる
// ✅ 良い例:必要な値だけを抽出して渡す
// (Server Component)
const user = await db.user.findUnique({ id: 1 });
return <UserAvatar src={user.image} name={user.name} />; // 最小限のデータ転送
13. 関数を直接サーバーで実行する Server Actions を活用する
フォームの送信やボタンクリックによるデータ更新を、型安全かつシンプルに記述できます。
これにより、クライアント側のJSコード(fetch処理など)を削減でき、ネットワークの往復も最適化することができます。
// ✅ 良い例: React 19 / Next.js の Server Action
// (Actions file)
'use server';
export async function updateUsername(formData: FormData) {
const name = formData.get('name');
await db.user.update({ name }); // サーバー側で直接DB操作
}
// (Client Component)
'use client';
import { updateUsername } from './actions';
export function NameForm() {
return (
<form action={updateUsername}>
<input name="name" />
<button type="submit">名前を更新</button>
</form>
);
}
14. useEffect でデータを取るのではなく、可能な限りサーバーコンポーネント内で取得する
サーバーはデータベースやAPIに近い場所へあります。
サーバーで取得することで、クライアント〜サーバー間の往復(Round Trip)を減らし、認証トークンなどの機密情報もブラウザに晒さず済みます。
// ✅ 良い例: 非同期サーバーコンポーネント
// (Server Component)
export default async function ProductList() {
// ブラウザからの fetch ではなく、直接DBや高速な内部ネットワークで取得
const products = await db.product.findMany();
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
15. 同一リクエスト内での重複したデータ取得を React の cache() 関数で防ぐ
コンポーネントツリーの深い場所で同じデータ(例:ログインユーザー情報)が必要な場合、Propsでバケツリレーをする代わりに、各所で同じ関数を呼び出しても、cache() を使っていれば実際の取得は1回だけで済みます。
import { cache } from 'react';
// ✅ 良い例: 同じリクエスト内なら何度呼んでも実行は1回だけ
export const getUser = cache(async (id: string) => {
console.log('DBへ問い合わせ中...');
return await db.user.findUnique({ id });
});
// コンポーネントAでもBでも getUser(id) を呼んでOK。実行は1回!
16. 必要なデータのみをフェッチする
サーバー内での処理であっても、DBから不要なデータをメモリに読み込むのはコストです。
特に大きなテキストフィールドやバイナリデータがある場合、取得範囲を絞ることでレスポンス速度が向上できます。
一見Reactのrulesっぽくないですが、React Server Components(RSC)の登場によってこのようなクエリ関係のrulesも意識されているのでしょう。
// ❌ 良くない例
const users = await db.user.findMany(); // 全てのカラムを取得
// ✅ 良い例
const users = await db.user.findMany({
select: {
id: true,
username: true, // 必要なものだけに絞る
}
});
🔄 クライアントサイドのデータ取得 優先度:MEDIUM-HIGH
このカテゴリーでは「いかにブラウザ側で賢くデータを管理し、ユーザーを待たせないか」を焦点としたセクションになっています。
17. fetch を生の useEffect でラップせず、データ取得ライブラリ(SWR / TanStack Query)を活用する
自前で useEffect を使うと、キャッシュ管理、重複リクエストの排除、再試行(Retry)などの実装が非常に困難になります。
専用ライブラリを使うことで、これらが自動化され、アプリの堅牢性が向上します。
import useSWR from 'swr';
// ✅ 良い例: SWRなどのライブラリに任せる
function UserProfile({ id }: { id: string }) {
const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher);
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラーが発生しました</div>;
return <div>{data.name}</div>;
}
18. コンポーネントのマウント時に useEffect でデータを取得するのをやめる
useEffect でのフェッチは、コンポーネントがレンダリングされた後に開始されるため、描画が遅れます。
前述のライブラリを使うか、可能な限りサーバーコンポーネントでデータを取得して初期値として渡すのがベストです。
// ❌ 良くない例: レンダリング後にフェッチが始まる(遅い)
useEffect(() => {
fetchData().then(setData);
}, []);
// ✅ 良い例: サーバーコンポーネントから初期データを注入する
// (Server Component)
const initialData = await fetchData();
return <ClientComponent fallbackData={initialData} />;
19. サーバーのレスポンスを待たずに、UIを先に更新する楽観的更新 (Optimistic Updates) をする
「いいね」ボタンや削除操作など、成功する確率が高い操作において、サーバーからの成功通知を待たずに画面を書き換えます。
これにより、ユーザーは「一瞬」で操作が完了したように感じます。
import { useOptimistic } from 'react';
// ✅ 良い例: React 19 の useOptimistic を活用
function LikeButton({ initialLikes }: { initialLikes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state) => state + 1 // 即座に反映されるロジック
);
return (
<button onClick={async () => {
addOptimisticLike(null); // 画面を先に更新
await updateLikeOnServer(); // 裏でサーバー通信
}}>
❤️ {optimisticLikes}
</button>
);
}
20. フィルタリングや検索条件などの状態(State)を URL のクエリパラメータで管理する
useState だけで管理すると、ページをリロードしたり、URLを友人に共有したりしたときに状態が消えてしまいます。
URLに状態を持たせることで、ブラウザの「戻る」ボタンも正常に機能し、ユーザー体験が向上します。
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export function SearchBar() {
const searchParams = useSearchParams();
const { replace } = useRouter();
const pathname = usePathname();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) params.set('query', term); else params.delete('query');
replace(`${pathname}?${params.toString()}`); // ✅ URLを更新
}
return <input onChange={(e) => handleSearch(e.target.value)} defaultValue={searchParams.get('query')?.toString()} />;
}
21. ホバーなどのユーザー操作時にプリフェッチを実施する
クリックしてからデータを取るのではなく、ホバーした(=クリックする可能性が高い)タイミングで fetch を開始します。
クリックしたときには既にデータがある、あるいは取得が進んでいる状態になり、待ち時間がゼロに近づきます。
import { useQueryClient } from '@tanstack/react-query';
import { fetchProduct } from './api';
export function ProductLink({ id, name }: { id: string; name: string }) {
const queryClient = useQueryClient();
// ✅ ホバー時に実行する関数
const prefetchProductData = async () => {
// 既にキャッシュがある場合は何もしない、なければフェッチを開始
await queryClient.prefetchQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
staleTime: 1000 * 60, // 1分間は「新鮮」とみなす
});
};
return (
<a
href={`/product/${id}`}
onMouseEnter={prefetchProductData} // 💡 マウスが乗った!
onFocus={prefetchProductData} // 💡 キーボードでフォーカスした!
className="p-4 border rounded hover:bg-gray-50"
>
{name} を見る
</a>
);
}
⚡ 再レンダリングの最適化 優先度:MEDIUM
Reactの基本原則である「状態が変われば下位のコンポーネントも全て再描画する」を いかに上手く抑制するか? がこのカテゴリーの主目的です。
22. 状態(State)を、それを使用する最小単位のコンポーネントに閉じ込める
親コンポーネントで useState を使うと、その状態が変わるたびに全く関係ない子コンポーネントまで再レンダリングされてしまいます。
状態を末端に移動させるだけで、この連鎖を断ち切ることができます。
// ❌ 良くない例: inputを入力するたびに <HeavyComponent /> が再描画される
function Parent() {
const [text, setText] = useState("");
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<HeavyComponent />
</div>
);
}
// ✅ 良い例: 入力ロジックを独立させ、Parentの再描画を防ぐ
function Parent() {
return (
<div>
<InputField />
<HeavyComponent />
</div>
);
}
function InputField() {
const [text, setText] = useState("");
return <input value={text} onChange={(e) => setText(e.target.value)} />;
}
23. コンポーネントを children や props として渡すことで再描画を回避する
Reactは「Propsとして渡された要素」は、その中身が変化していない限り、親が再レンダリングされても再評価をスキップする特性があります。
// ✅ 良い例: Wrapperが再描画されても、childrenである HeavyComponent は無風
function ScrollWrapper({ children }: { children: React.ReactNode }) {
const [scrollY, setScrollY] = useState(0); // スクロールで頻繁に更新
return <div onScroll={(e) => setScrollY(e.currentTarget.scrollTop)}>{children}</div>;
}
// 呼び出し側
<ScrollWrapper>
<HeavyComponent />
</ScrollWrapper>
24. コンポーネントをメモ化する (React.memo)
純粋関数として「同じ入力なら同じ出力」になるコンポーネントは memo で囲むことで、Propsが変わらない限り、コンポーネントの再レンダリングを完全にスキップできます。
ただし、何でも囲むと「比較のコスト」が上回るため、重いコンポーネントに絞るのが重要になります。
import { memo } from 'react';
// ✅ 良い例: プロパティが変わらない限り、再計算を避ける
const ExpensiveComponent = memo(({ data }: { data: string }) => {
return <div>{/* 複雑なレンダリング */}</div>;
});
25. 重い計算をメモ化する (useMemo)
useMemo を関数に指定することで、計算結果をキャッシュし、依存配列が変わらない限り再計算しないように制御することができます。
ただしこちらも memo と同様に、重い処理に使用しないと「比較のコスト」が上回ってしまう原因となります。
// ✅ 良い例: items か query が変わった時だけ実行
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(query));
}, [items, query]);
26. コールバック参照を安定させる (useCallback)
useCallback は関数の実体をキャッシュし、レンダリングのたびに新しい関数が生成されるのを防ぎます。
React.memo を使っている子コンポーネントに関数を渡す場合、親が再描画されるたびに関数が新しくなると memo が効かなくなってしまいます。
useCallbackを使えばそのような再レンダリングを防ぐことができます。
// ✅ 良い例: handleClick の参照が変わらないので、memo化した子の再描画を防げる
const handleClick = useCallback(() => {
console.log("Clicked!");
}, []);
return <MemoizedButton onClick={handleClick} />;
27. Props にはオブジェクト全体を渡すのではなく、必要な値(文字列や数値)だけを渡す
オブジェクト全体を渡すと、中身が変わっていなくても参照が変わると「変化した」と判定されます。
数値や文字列なら値そのもので比較されるため、安定させることができます。
// ❌ 良くない例: userオブジェクトが変わるたびに再描画
<UserAvatar user={user} />
// ✅ 良い例: nameという文字列だけを見るので安定する
<UserAvatar name={user.name} src={user.image} />
28. 巨大な Context を小さな単位に分割する
1つの Context に「ユーザー情報」「テーマ」「設定」を全部詰め込むと、どれか1つが変わっただけで、その Context を使っている全コンポーネントが再描画されます。
関心事ごとに Provider を分けるのが鉄則になります。
// ✅ 良い例: テーマだけ変えたい時にユーザー情報のコンシューマーを邪魔しない
<UserProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</UserProvider>
29. useEffectでstateの更新を実施するのを避ける
useEffect 内で setState をすると、もう一度レンダリングが発生してしまいます(2回レンダリング)。
直接変数に計算結果を代入すれば、1回のレンダリングで済みます。
// ❌ 良くない例: レンダリングが2回走る
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 良い例: レンダリング中に計算(高速・クリーン)
const fullName = `${firstName} ${lastName}`;
🎨 レンダリングのパフォーマンス 優先度:MEDIUM
再レンダリングの最適化が「Reactの計算を減らす」カテゴリーだったのに対して、今回は「ブラウザのレンダリング処理をいかにスムーズにするか」を目的としたカテゴリーになります。
言い換えると「ユーザーにストレスを感じさせないようにする」がこのカテゴリーの目標です。
30. 画面に見えている要素だけをレンダリングし、見えない要素はDOMから取り除く
数千件のデータを一気に map で回すと、目に見えない部分までDOMが作成され、メモリ消費と初期描画が激重になります。
仮想化(Windowing)を使うと、スクロールに合わせて必要な分だけDOMを生成・再利用するため、常に軽快に動くことができます。
import { FixedSizeList as List } from 'react-window';
// ✅ 良い例: 10,000件あっても、実際に作成されるDOMは画面内の数件だけ
const MyList = ({ items }: { items: string[] }) => (
<List
height={500}
itemCount={items.length}
itemSize={35}
width={300}
>
{({ index, style }) => (
<div style={style}>項目 {items[index]}</div>
)}
</List>
);
31. useTransitionを使って処理をバックグラウンドで実施する
検索フィルターの更新など、完了までに時間がかかる処理を startTransition で囲むと、Reactはその処理の優先度を下げます。
その間もユーザーの入力(タイピング)には即座に反応できるため、UIがフリーズした印象を与えません。
const [isPending, startTransition] = useTransition();
const [filterTerm, setFilterTerm] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// ✅ 入力自体は即座に反映(高優先)
setFilterTerm(value);
// ✅ 重い検索結果の計算はバックグラウンドで(低優先)
startTransition(() => {
updateFilteredList(value);
});
};
return (
<div>
<input onChange={handleChange} />
{isPending && <p>更新中...</p>}
</div>
);
32. useDeferredValue を使って値の遅延を実施する
useTransition が「動作(関数)」を遅延させるのに対し、こちらは「値」そのものを遅延させます。
ユーザーが高速で入力している間は古い値を表示し続け、入力が落ち着いたタイミングで重いレンダリングを開始します。
const [query, setQuery] = useState("");
// ✅ query の「遅延版」を作成
const deferredQuery = useDeferredValue(query);
// 重いコンポーネントには遅延版を渡す
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<HeavyList query={deferredQuery} />
</>
);
33. アニメーションには JavaScript ではなく CSSの transform を使う
JSで座標(top, left)を計算して動かすと、メインスレッドが計算に追われ、カクつきの原因になります。
CSSの transform はブラウザの GPU を活用できるため、メインスレッドが重くても滑らかに動かすことができます。
/* ❌ 良くない例: JSで style.left を毎フレーム書き換える */
/* ✅ 良い例: CSSで動かす */
.box {
transition: transform 0.3s ease;
}
.box:hover {
transform: translateX(100px); /* GPUで処理される */
}
34. 画像や広告などのコンテンツが表示される領域をあらかじめ確保しておく
コンテンツが後から読み込まれて画面が「ガクッ」と動く現象(CLS)は、ユーザーの誤操作を招き、パフォーマンス指標も悪化させます。
高さ・幅を指定するか、アスペクト比を固定して領域を予約します。
// ✅ 良い例: 画像が読み込まれる前から場所を確保しておく
<div style={{ aspectRatio: '16 / 9', backgroundColor: '#f0f0f0' }}>
<img src={url} alt="Product" style={{ width: '100%', height: 'auto' }} />
</div>
🚀 高度なパターン 優先度:LOW-MEDIUM
こちらは「知らなくてもアプリは問題なく動く&致命的に遅くなることもないが、知っていればより高度なコード(保守性・可読性の向上)を書けるよ」という内容のカテゴリーになっています。
35. カスタムフックによりロジックの隠蔽をする
コンポーネントの見た目(JSX)とロジックを分離することで、テストがしやすくなり、他のコンポーネントでの再利用も容易になります。
また、コンポーネント自体がスッキリして可読性を向上させることができます。
// ✅ 良い例: ロジックをフックに閉じ込める
function useChatRoom(roomId: string) {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
const connection = connectToRoom(roomId);
connection.onMessage(msg => setMessages(prev => [...prev, msg]));
return () => connection.disconnect();
}, [roomId]);
return messages;
}
// コンポーネント側は「何を表示するか」に集中できる
function ChatRoom({ roomId }: { roomId: string }) {
const messages = useChatRoom(roomId);
return <ul>{messages.map(m => <li key={m.id}>{m.text}</li>)}</ul>;
}
36. Props としてコンポーネントそのものを渡すことで、Prop Drilling(バケツリレー)を防ぐ
深い階層にデータを渡すために中間コンポーネントを関与する必要がなくなります。
また、前述の「再レンダリング最適化(childrenを使う)」と同様、パフォーマンス向上にも寄与します。
// ✅ 良い例: 「枠」だけ作って、中身(スロット)は親から流し込む
function Layout({ leftSlot, rightSlot }: { leftSlot: React.ReactNode, rightSlot: React.ReactNode }) {
return (
<div className="flex">
<aside>{leftSlot}</aside>
<main>{rightSlot}</main>
</div>
);
}
// 使う側: 階層を飛び越えて直接配置できる
<Layout
leftSlot={<Navigation user={user} />}
rightSlot={<Dashboard data={data} />}
/>
37. レンダリングのたびに新しいオブジェクトや配列が作られないようにする
propsにそのままオブジェクト(または配列)を渡すと、見た目は同じでもレンダリングのたびに新しいオブジェクトがメモリ上に作られます。
これが React.memo 化された子コンポーネントの「不要な再描画」を引き起こす原因になります。
// ❌ 良くない例: 毎回新しいオブジェクトが作られ、MemoizedChild が再描画される
function Parent() {
return <MemoizedChild config={{ theme: 'dark' }} />;
}
// ✅ 良い例: コンポーネントの外で定義するか、useMemo を使う
const DARK_CONFIG = { theme: 'dark' }; // 参照が一生変わらない
function Parent() {
return <MemoizedChild config={DARK_CONFIG} />;
}
38. サーバー側の機密情報(パスワードやAPIキーなど)が、誤ってクライアントに送信されないように taint を設定する
React Server Components (RSC) 特有の機能です。
サーバー専用のオブジェクトに taint を設定しておくと、もし誤ってそれをクライアントコンポーネントの Props に渡そうとした場合、Reactがビルド時や実行時にエラーを出して防いでくれます。
// (Server-only file: user-service.ts)
import { experimental_taintUniqueValue } from 'react';
export async function getUserSession(id: string) {
const user = await db.user.findUnique({ id });
// ✅ パスワードを「汚染」としてマーク。これがクライアントに渡されるとエラーになる
experimental_taintUniqueValue(
'Do not pass passwords to the client!',
user,
user.passwordHash
);
return user;
}
さいごに
AIに知識を付けるため定義されたrulesをここまで見てきましたが、人間が読んでも良質なインプット教材として活用できるなと思いました。
あとはこちらからプロジェクトに合わせたrulesをピックアップして、Skillsとして導入をしていければなと思っています。
今回は触れませんでしたが、優先度が付いていない「UX・アクセシビリティ」系のrulesもあるようなので、気になった方はチェックしてみてください!