0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactの再レンダリングを調査する方法と不要な更新を減らす実装例

0
Posted at

Reactを用いたWebアプリケーション開発において、コンポーネントの不要な再レンダリングはパフォーマンス低下の要因となります。本記事では、実務で使える再レンダリングの調査方法と、不要な更新を削減するための具体的な実装パターン、およびチューニングの判断基準を解説します。

読者が抱える課題

  • アプリケーションの動作が重いが、どのコンポーネントが原因で再レンダリングされているか特定できない
  • React.memouseCallback を導入したものの、意図した通りに再レンダリングが抑制されていない
  • どのような基準でメモ化を適用すべきか、チーム内での共通指針がない

この記事で分かること

  • React Developer Tools を用いた再レンダリング原因の調査手順
  • 不要な再レンダリングを削減する3つの実装パターン(良い例・悪い例)
  • メモ化を適用する際の判断基準チェックリスト

前提条件

  • React 18以降
  • TypeScriptを使用した関数コンポーネントの開発環境

1. 再レンダリング原因の調査手順

不要な再レンダリングを削減する前に、まずは「どこが」「なぜ」再レンダリングされているかを特定する必要があります。ブラウザの拡張機能である React Developer Tools を使用した調査手順を解説します。

手順1: Profilerによる計測

  1. 開発者ツールの「Profiler」タブを開きます。
  2. 歯車アイコン(Profiler Settings)をクリックし、"Record why each component rendered while profiling." にチェックを入れます。これにより、再レンダリングの理由(どのPropsやStateが変化したか)が記録されます。
  3. 録画ボタン(青い丸)を押し、調査したい操作(ボタンクリックや入力など)を画面上で行います。
  4. 録画を停止します。

手順2: ボトルネックの特定

  • Flamegraph チャート: 各コンポーネントのレンダリング所要時間が色別(黄色が重く、青が軽い)で表示されます。幅が広いコンポーネントほど処理時間がかかっています。
  • Ranked チャート: レンダリング時間が長かった順にコンポーネントが並びます。パフォーマンスに影響を与えている上位のコンポーネントを特定するのに適しています。

手順3: 原因の確認

特定のコンポーネントをクリックすると、右側の詳細パネルに "Why did this render?" という項目が表示されます。

  • Props changed: [key]
  • State changed
  • Hook changed
    などの情報から、不要な更新を引き起こしているトリガーを特定します。

2. 不要な更新を減らす実装パターン

原因を特定した後、コードレベルで最適化を行います。よくある原因と対策の実装例を紹介します。

パターンA: オブジェクト・関数参照の固定化

親コンポーネントがレンダリングされるたびに、インラインで定義されたオブジェクトや関数は新しい参照(異なるメモリ番地)として生成されます。これにより、子コンポーネントが React.memo 化されていても再レンダリングが発生します。

悪い例(毎回新しい参照が作られる)

import React, { useState } from 'react';

const ChildComponent = React.memo(({ onClick, options }: { onClick: () => void; options: { theme: string } }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>{options.theme}</button>;
});
ChildComponent.displayName = 'ChildComponent';

export const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 親のStateが更新されるたびに、新しい関数とオブジェクトが生成される
  const handleClick = () => console.log('clicked');
  const config = { theme: 'dark' };

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment ({count})</button>
      <ChildComponent onClick={handleClick} options={config} />
    </>
  );
};

良い例(useCallback と useMemo による参照の固定)

import React, { useState, useCallback, useMemo } from 'react';

const ChildComponent = React.memo(({ onClick, options }: { onClick: () => void; options: { theme: string } }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>{options.theme}</button>;
});
ChildComponent.displayName = 'ChildComponent';

export const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 依存配列が空のため、初回レンダリング時の参照を維持する
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  const config = useMemo(() => ({
    theme: 'dark'
  }), []);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment ({count})</button>
      <ChildComponent onClick={handleClick} options={config} />
    </>
  );
};

パターンB: コンポーネント構成の工夫(Childrenの活用)

メモ化(useMemouseCallback)を多用するとコードの可読性が下がり、依存配列の管理コストが増加します。コンポーネントの構造を変更することで、メモ化を使わずに再レンダリングを防ぐ手法が有効です。

悪い例(重いコンポーネントが巻き添えで再レンダリングされる)

import React, { useState } from 'react';

const HeavyComponent = () => {
  // レンダリングコストが高いコンポーネント
  console.log('HeavyComponent rendered');
  return <div>Heavy Content</div>;
};

export const ScrollContainer = () => {
  const [scrollPosition, setScrollPosition] = useState(0);

  return (
    <div onScroll={(e) => setScrollPosition(e.currentTarget.scrollTop)}>
      <p>Scroll: {scrollPosition}</p>
      {/* スクロールするたびにHeavyComponentも再レンダリングされる */}
      <HeavyComponent />
    </div>
  );
};

良い例(children として受け取ることで再レンダリングを回避)

import React, { useState } from 'react';

const HeavyComponent = () => {
  console.log('HeavyComponent rendered');
  return <div>Heavy Content</div>;
};

// スクロール状態を管理するラッパーコンポーネント
const ScrollProvider = ({ children }: { children: React.ReactNode }) => {
  const [scrollPosition, setScrollPosition] = useState(0);
  return (
    <div onScroll={(e) => setScrollPosition(e.currentTarget.scrollTop)}>
      <p>Scroll: {scrollPosition}</p>
      {children}
    </div>
  );
};

export const App = () => {
  return (
    <ScrollProvider>
      {/* HeavyComponentはAppのコンテキストで評価されるため、ScrollProviderのState更新の影響を受けない */}
      <HeavyComponent />
    </ScrollProvider>
  );
};

3. 実務用:メモ化適用の判断基準チェックリスト

すべてのコンポーネントや関数を無条件にメモ化することは、メモリ消費や比較処理のオーバーヘッドを増やすため推奨されません。以下のチェックリストを基準に、適用の要否を判断してください。

項目 チェック内容 適用推奨度 理由・対策
1 子コンポーネントの描画コストが非常に高い(グラフ描画、大量のリストなど) 高 (適用すべき) React.memo を適用し、Propsの変更時のみ再描画させる。
2 子コンポーネントにオブジェクトや関数をPropsとして渡している 中 (状況による) 子コンポーネントが React.memo 化されている場合のみ、親側で useMemo / useCallback を使用する。
3 プリミティブ値(string, number, boolean)のみをPropsに持つ 低 (不要な場合が多い) Reactのデフォルトの比較処理が高速なため、描画コストが低ければメモ化のメリットが少ない。
4 頻繁に値が変化するStateに直接依存している 低 (効果がない) 毎フレーム値が変わるような処理では、メモ化の比較処理自体がオーバーヘッドになる。

4. 導入時の注意点とよくある失敗

依存配列(Dependency Array)の指定漏れ

useCallbackuseMemo を使用する際、内部で参照しているStateやPropsを依存配列に指定し忘れると、古い値(クロージャによる古い状態)を参照し続けるバグ(Stale Closure)の原因になります。
対策: eslint-plugin-react-hooksexhaustive-deps ルールを有効化し、静的解析で検知できるように設定してください。

React.memo の単体適用

親コンポーネントから渡す関数やオブジェクトがメモ化されていない状態で、子コンポーネントに React.memo だけを適用しても、参照が変わるため再レンダリングを防げません。必ず「渡す側の参照固定」と「受け取る側のメモ化」をセットで設計してください。


まとめ

Reactのパフォーマンスチューニングは、推測ではなく React Developer Tools による計測 から始めることが鉄則です。不要な再レンダリングを発見した際は、安易にすべてをメモ化するのではなく、コンポーネントの分割や children を使った構造の最適化を検討し、それでも解決しない場合に React.memouseCallback を適切に組み合わせて適用してください。

※Reactの将来のバージョン(React Compilerなど)では、これらの手動最適化が自動化される可能性があります。実装の際は、利用しているReactバージョンの最新の公式ドキュメントを確認してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?