最新記事公開時にプッシュ通知します

Reactコンポーネントの不要な再レンダリングを制御する。useMemo/useCallbackの使いどころとPropsの粒度管理

2025年11月17日

Reactのレンダリング最適化

執筆

中川 幸哉

有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)に所属するテクニカルライター。新潟県上越市出身。会津大学コンピュータ理工学部卒業後、現在は新潟市に在住。ReactやAndroidを軸に、モバイルアプリ開発やWebサイト制作、Webメディア編集部の業務改善や、プログラミング技術記事の執筆等に携わっている。著書に「たった1日で基本が身に付く! Androidアプリ開発超入門」(技術評論社)、「基礎から学ぶ React Native入門」(翔泳社)他。

監修

山田 祥寛

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。
主な著書に「独習」シリーズ、「これからはじめるReact実践入門」、「改訂3版 JavaScript本格入門」他、著書多数

はじめに:Propsのパフォーマンス最適化

前回はCompound ComponentsパターンとTypeScriptによる型安全なProps設計について学びました。今回は、その設計を踏まえて、アプリケーションの実行時パフォーマンスに直結するPropsの最適化テクニックについて解説します。

適切なPropsの扱い方は、コンポーネントの再レンダリングを制御し、ユーザーエクスペリエンスの向上に大きく貢献します。

不要な再レンダリングを防ぐテクニック

Propsの渡し方は、アプリケーションの再レンダリング性能に大きな影響を与えます。特に重要なのは、参照の同一性とProps粒度の適切な管理です。毎回新しいオブジェクトや関数を作成することは、子コンポーネントの不要な再レンダリングを引き起こします。

// ❌ 毎回新しいオブジェクト・関数が作成される
function ParentComponent() {
  return (
    <ChildComponent
      style={{ marginTop: 10 }}  // 毎回新しいオブジェクト
      onAction={() => console.log('action')}  // 毎回新しい関数
    />
  );
}

// ✅ 安定した参照を提供
const ChildComponent = React.memo(({ style, onAction }) => {
// 子コンポーネントの実装
  return <div style={style} onClick={onAction}>Child</div>;
});
function ParentComponent() {
  //useMemo: 計算結果をメモ化し、依存配列が変わらない限り同じ参照を保持
  const style = useMemo(() => ({ marginTop: 10 }), []);
  // useCallback: 関数をメモ化し、依存配列が変わらない限り同じ参照を保持
  const handleAction = useCallback(() => console.log('action'), []);
  return <ChildComponent style={style} onAction={handleAction} />;
}

 

メモ化の適切な使用指針

ただし、useMemo/useCallbackは万能薬ではありません。これらのフック自体にも実行コストがあるため、過剰な使用は逆にパフォーマンスを悪化させる可能性があります。

メモ化を使用すべき場面として、重い計算処理がある場合、そして他のフックの依存配列に含まれる値がある場合が挙げられます。一方で、避けるべき場面としては、プリミティブ値(string、number等)のメモ化、軽い計算処理のメモ化、そして一度しか実行されないコンポーネントでの使用が該当します。

重い計算処理の目安として、useMemoの公式ドキュメントには「計算コストが高いかどうかを見分ける方法」というコラムがあります。この中では例え話として、「1ミリ秒以上かかっている処理があればそれはメモ化する意味があるかもしれない(意訳)」といった目安が示されています。再レンダリングの際にカクカクするな、などと感じた際に、実行時間を計測してみるとよいでしょう。

以下は、メモ化が不要なケースの典型例と、適切なケースの例です。

// ❌ 不要なメモ化:プリミティブ値や軽い処理
const count = useMemo(() => items.length, [items]); // items.lengthは軽い処理
const isEven = useMemo(() => number % 2 === 0, [number]); // プリミティブな計算

// ✅ 適切なメモ化:重い処理や複雑なオブジェクト
const expensiveValue = useMemo(() => {
  return items.reduce((acc, item) => {
    // 重い計算処理
    return acc + complexCalculation(item);
  }, 0);
}, [items]);

 

Props粒度の適切な管理

もう一つの重要な最適化手法が、Props粒度の適切な管理です。大きなオブジェクトをそのまま渡すのではなく、必要なデータのみを抽出して渡すことで、不要な再レンダリングを防げます。以下の例では、コンポーネントが実際に必要とする情報のみをPropsとして受け取るように設計しています。

// ❌ 大きなオブジェクトをそのまま渡す
function PostComponent({ data }) {
  return <div>投稿者: {data.user.name}</div>; // userの情報しか使わない
}

// ✅ 必要なデータのみを渡す
function PostComponent({ authorName }) {
  return <div>投稿者: {authorName}</div>;
}

この粒度の細かいProps選択は、Reactの再レンダリング最適化において極めて重要です。Reactは、Propsの変更を検知してコンポーネントの再レンダリングを判断します。必要最小限のPropsだけを渡すことで、不要な再レンダリングが発生する機会を減らせます。

逆に、大きなオブジェクトをそのまま渡してしまうと、コンポーネントが実際には使用していないプロパティの変更でも再レンダリングが発生する可能性があります。このため、Propsを渡す際に粒度を意識することが、パフォーマンス最適化の第一歩となります。

避けるべきアンチパターン

Props設計において、パフォーマンスと保守性を損なう代表的なアンチパターンを確認しましょう。

1. Props Drilling(バケツリレー)

前編のCompound Componentsでも触れましたが、深い階層にわたってPropsを受け渡すのは保守性とパフォーマンスの両面で問題となります。

▲深い階層へ渡していくのはアンチパターン
// ❌ 中間コンポーネントが関心のないPropsを受け渡し
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} onUserChange={setUser} />;
}

function Layout({ user, onUserChange }) {
  // Layoutはuserに関心がないのに、Headerのために受け渡している
  return (
    <div>
      <Header user={user} onUserChange={onUserChange} />
      <Main />
    </div>
  );
}

// ✅ Contextで解決
const UserContext = React.createContext(null);

function App() {
  const [user, setUser] = useState(null);
  return (
    <UserContext value={{ user, updateUser: setUser }}>
      <Layout />
    </UserContext> {/* 註: React 19からは.Providerが非推奨になりました */}
  );
}

function Header() {
  const { user, updateUser } = useContext(UserContext);
  return <div>Welcome, {user?.name}</div>;
}

Contextを使うことで、中間コンポーネントは不要なPropsの受け渡しから解放され、本来の責務に集中できます。また、userに関連する値が変更されても、Layoutコンポーネントは再レンダリングされません。必要なコンポーネント(Header)だけが更新されるため、パフォーマンスも向上します。

ただし、Contextの過度な使用は別の問題を引き起こす可能性があります。例えば、Context値が変更されるとそれを使う全てのコンポーネントが再レンダリングされてしまいます。また、Contextを使うとデータがどこから来ているのか分かりにくくなり、デバッグが難しくなることもあります。テストを書く際も、毎回Context Providerを用意する必要が出てきます。こうした理由から、Props Drillingが2〜3階層程度であれば、素直にPropsで渡す方がシンプルな場合も多いのです。

2. 複雑すぎるProps設計

一つのコンポーネントにあまりに多くの責務を持たせると、Props設計が複雑になり保守性が悪化します。

// ❌ 複雑すぎるProps
interface ComplexButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'sm' | 'md' | 'lg';
  icon?: IconType;
  iconPosition?: 'left' | 'right';
  loading?: boolean;
  disabled?: boolean;
  tooltip?: string;
  tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
  badge?: number;
  badgeColor?: 'red' | 'blue' | 'green';
  // ... さらに続く
}

// ✅ 責務を分離
function Button({ variant, size, children, ...props }) {
  return <button className={`btn btn-${variant} btn-${size}`} {...props}>{children}</button>;
}

function IconButton({ icon, position = 'left', children, ...buttonProps }) {
  return (
    <Button {...buttonProps}>
      {position === 'left' && <Icon type={icon} />}
      {children}
      {position === 'right' && <Icon type={icon} />}
    </Button>
  );
}

function BadgeButton({ badge, badgeColor, children, ...buttonProps }) {
  return (
    <div className="relative">
      <Button {...buttonProps}>{children}</Button>
      {badge && <Badge count={badge} color={badgeColor} />}
    </div>
  );
}

このように責務を分離することで、各コンポーネントは明確な役割を持ち、再利用性が高まります。Button、IconButton、BadgeButtonは、それぞれ独立して使用でき、テストも容易になります。

また、新しい機能を追加する際も、既存のコンポーネントを組み合わせるだけで実現できるため、保守性が大きく向上します。複雑なProps設計に直面したら、それは「コンポーネントの責務が大きすぎる」というサインかもしれません。

まとめ

Propsの実践的な最適化テクニックを2回にわたって学んできました。Compound Componentsパターン、TypeScript型安全設計、パフォーマンス最適化は、現代のReact開発で不可欠なスキルです。前編で学んだProps設計(コンポーネント間の「契約設計」)に加えて、本記事で扱った最適化テクニックを適用することで、使いやすく、パフォーマンスに優れた、長期的にメンテナンスしやすいコンポーネントを実現できます。

今回学んだ内容は、React 19で安定版が実装されたReact Compilerの基礎知識としても有用です。React Compilerは、useMemo/useCallback/React.memoによる手動メモ化を自動化する機能で、ビルド時にコードを解析して最適化を適用します。これにより、開発者はメモ化を意識せずにパフォーマンスの高いコンポーネントを実装できるようになります。ただし、本記事で解説した「なぜメモ化が必要なのか」「どのような場合に最適化すべきか」という原則の理解は、React Compilerを使う場合も、その動作を理解し適切に活用する上で重要です。

次回は「状態管理の実践的使い分け」について解説します。

関連記事

人気記事

  • コピーしました

RSS
RSS