メインコンテンツに移動

不要な再レンダリングを避ける方法とは?コンポーネント設計と実装の改善ポイントを解説

フロントエンド開発において、再レンダリングは避けるべきものとして語られることが多いですが、実際には「再レンダリングそのもの」が悪いわけではありません。UI は state や props が変われば再描画されるのが自然であり、それ自体はフレームワークの正常な動作です。本当に問題になるのは、表示上は変わっていないのに何度も描画処理が走っていたり、更新の影響範囲が必要以上に広くなっていたりするケースです。つまり、避けるべきなのは再レンダリング全般ではなく、不要な再レンダリング です。この違いを理解しないまま最適化を始めると、本来必要な更新まで抑えようとしてしまい、かえってコードが複雑で読みにくくなることがあります。

また、不要な再レンダリングの問題は、単に処理速度の問題だけではありません。コンポーネントの責務が曖昧だったり、state の粒度が粗すぎたり、props の設計が安定していなかったりすると、パフォーマンスの問題としてだけでなく、保守性の低さとしても表れます。たとえば、親コンポーネントが少し更新されるたびに広い子孫ツリーまで巻き込まれて再描画されるような設計では、「どの更新がどこに影響したのか」が見えにくくなり、バグの調査や改善も難しくなります。つまり、不要な再レンダリングを減らすことは、単なる高速化ではなく、コンポーネント設計そのものを整理することにもつながります。本記事では、React を中心に、不要な再レンダリングが起こる原因と、それを避けるための実践的な考え方を 12 の観点から整理していきます。

1. まず理解したいのは「再レンダリングは悪ではない」ということ

再レンダリングという言葉が出てくると、どうしても「回数は少ないほどよい」と考えたくなります。しかし、コンポーネントベースの UI において、再レンダリングは state や props の変化を画面へ反映するための基本動作です。つまり、再レンダリングそのものを敵視すると、必要な更新まで避けようとしてしまい、実装が不自然になりやすくなります。本当に見るべきなのは、「その再レンダリングは意味のあるものか」「UI の変化に対して妥当なコストか」という点です。変わるべき表示が変わるための再レンダリングであれば、それは最適化の対象ではなく、むしろ正しく起きているべき挙動です。

問題になるのは、見た目が変わらないにもかかわらず毎回描画されるケースや、ある小さな更新が広い範囲に波及してしまうケースです。たとえば、入力欄ひとつの変化のたびに大きな親コンポーネントと無関係な子コンポーネントまで再レンダリングされるなら、それは設計上の見直し余地があります。つまり、最適化を始める前に、「何が不要で、何が必要か」を区別することが重要です。この前提がないまま memouseMemo を増やしても、根本的な問題を見失いやすくなります。

1.1 必要な再レンダリングと不要な再レンダリングの違い

必要な再レンダリングとは、state や props の変化によって、実際に UI の見た目や意味が更新されるべきケースです。たとえば、入力値の表示が変わる、一覧の内容が変わる、ボタンの disabled 状態が変わる、といった場面では再レンダリングは自然です。これを減らすことよりも、正しく分かりやすく更新されることのほうが大切です。パフォーマンスを気にするあまり、本来あるべき更新まで止めようとすると、逆にデータと UI の整合性が崩れることもあります。

一方で、不要な再レンダリングとは、更新の結果としてコンポーネント関数が再実行されても、実質的に何も変わっていないケースです。たとえば、親の state 変更に引きずられて、表示内容も props も変わっていない子が毎回描画される場合がこれにあたります。この差を見極めることができると、「とにかく再レンダリングを減らす」のではなく、「不要な範囲にまで更新が広がらないようにする」という、より正しい最適化の方向が見えてきます。

2. state の粒度が粗いと再レンダリングは広がりやすい

不要な再レンダリングが起こる大きな原因の一つは、state の粒度が粗すぎることです。たとえば、ページ全体の大きなオブジェクト state を 1 つだけ持ち、入力欄の小さな変更もその中へ全部入れているような設計では、わずかな変更のたびに親コンポーネント全体が更新されやすくなります。そして、その親が再レンダリングされると、そこからぶら下がる子コンポーネントまで影響を受けやすくなります。つまり、小さな変更に対して更新範囲が大きすぎる状態になりやすいです。

これは、管理する state を 1 箇所へ集めすぎることでも起こります。たしかに state をまとめると一見分かりやすく感じられることもありますが、UI ごとの責務や変更の局所性を無視してしまうと、結果として余計な再レンダリングを増やすことになります。最適化の第一歩として重要なのは、「その state は本当にその範囲で持つべきか」を見直すことです。ローカルで閉じられる state はローカルへ寄せ、共有すべき state だけを上位へ持ち上げるようにすると、再レンダリングの波及範囲をかなり抑えやすくなります。

2.1 ローカルで持てる state はローカルで持つ

コンポーネントの外側へ持ち上げられた state は、その親を再レンダリングさせやすくなり、結果として関係のない子まで巻き込みます。たとえば、モーダルの開閉や、ちょっとした hover 状態、入力途中の一時値のように、その場だけで完結する情報まで大きな親へ持たせると、更新コストが必要以上に広がります。これらはローカル state のままで十分なことが多く、むしろそのほうが設計として自然です。

大切なのは、「共有できるか」ではなく「共有する必要があるか」で判断することです。どこでも参照できる状態にしておくことは便利に見えますが、その代わりに更新範囲が広がるなら、実務上のコストは高くなります。つまり、state の粒度を適切に分けることは、不要な再レンダリングを防ぐためだけでなく、責務の明確化にもつながります。

2.2 state をまとめすぎると小さな変更が全体へ波及する

複数の値をひとつの巨大な state オブジェクトへまとめてしまうと、更新のたびにオブジェクト参照が変わりやすくなります。その結果、本当は一部しか変わっていなくても、「全体が新しい値になった」として扱われ、関連コンポーネントが広く再レンダリングされる原因になります。とくにフォームや設定画面のように細かい値が多い UI では、この問題が目立ちやすいです。

もちろん、state をまったく分けないほうがよいという話ではありません。重要なのは、更新の単位と責務の単位がある程度一致していることです。頻繁に別々に変わるものを無理にひとつへまとめると、小さな更新が全体へ波及しやすくなります。つまり、不要な再レンダリングを防ぐには、state の構造を「管理しやすさ」だけでなく「更新の局所性」からも見直す必要があります。

3. 親の再レンダリングがそのまま子へ広がる構造を見直す

React では、親コンポーネントが再レンダリングされると、その子コンポーネントも再評価されやすくなります。これは React の自然な動作ですが、設計によっては更新範囲が必要以上に広がる原因になります。たとえば、親コンポーネントが多くの state を持ち、その中の小さな更新だけで、表示上まったく関係のない子コンポーネントまで毎回再レンダリングされているなら、それは見直しの余地があります。大きな親が何でも抱え込む構造は、一見すると分かりやすそうでいて、実際には更新の責務を集中させすぎていることが多いです。

この問題を避けるには、コンポーネントの境界をどこで切るかが重要になります。頻繁に変わる部分と、ほとんど変わらない部分を同じ親の直下へ並べたままにしていると、更新のたびに静的な部分まで巻き込みやすくなります。逆に、責務ごとに分けられた構造なら、変化の多い領域だけを局所的に更新しやすくなります。つまり、不要な再レンダリングの問題は、単なる最適化テクニックではなく、親子関係の設計やコンポーネント分割の問題でもあります。

3.1 更新頻度の違う UI は分けておく

更新頻度が高い UI と、ほとんど変わらない UI を同じコンポーネントの中へ密集させると、少しの変更で全部が巻き込まれやすくなります。たとえば、入力中に頻繁に変わるフォーム領域と、ほぼ固定の説明文やサイドパネルを同じレンダーツリーの近い場所に置いていると、親の更新だけで静的な部分まで何度も再評価されることがあります。こうした構造では、表示上は変わらないのに関数は何度も実行されるため、見た目に出にくい無駄が蓄積しやすいです。

そのため、更新頻度が違う UI は、責務に応じて分けておいたほうがよいです。頻繁に変わる部分を小さなコンポーネントとして切り出し、変わりにくい部分は比較的安定した外側へ出しておくと、更新範囲をかなり抑えやすくなります。つまり、不要な再レンダリングを防ぐには、機能で分けるだけでなく、更新頻度でもコンポーネントを見直す視点が重要です。

3.2 コンポーネント分割は見た目だけでなく更新境界でも考える

コンポーネント分割というと、見た目や役割で分けることがまず思い浮かびます。もちろんそれは重要ですが、再レンダリング最適化の観点では、「どこで更新を止めたいか」という視点でも分割を考えるべきです。見た目上は一体に見える UI でも、内部的には更新境界を分けたほうがよいことがあります。逆に、見た目で細かく分けすぎても、更新の責務が同じ場所に残っていれば、再レンダリングの問題はあまり解決しません。

つまり、コンポーネント分割はファイルを小さくするためだけのものではなく、更新の影響範囲を制御するための設計手段でもあります。どこまでを一緒に再レンダリングしてよいのか、どこから先は独立して安定させたいのかを考えて境界を切ると、後から memo へ頼らなくても自然に無駄な再評価を減らせることが多いです。

4. React.memo は便利だが、使いどころを間違えると効果が薄い

不要な再レンダリングを避ける方法として、まず React.memo を思い浮かべる人は多いです。たしかに memo は、props が変わっていない子コンポーネントの再レンダリングを抑えるのに有効です。とくに、親が頻繁に更新される構造で、子の props は安定している場合には大きな効果が出ることがあります。しかし、memo は魔法の道具ではありません。props の参照が毎回変わっていれば、memo を付けても普通に再レンダリングされますし、比較コストのほうが高くつく場面もあります。

つまり、React.memo は「とりあえず付ければ速くなる」ものではなく、props が安定していて、なおかつ再レンダリングのコストが無視できないときに意味を持つ最適化です。親の設計や props の渡し方が不安定なままでは、memo をいくら付けても本質的な改善にはなりません。まずは state や props の設計を整え、そのうえで再レンダリングを避けたい境界に memo を置くほうが自然です。

4.1 memo が効くのは props が安定しているとき

React.memo は shallow comparison によって props の変化を見ています。そのため、文字列や数値のようなプリミティブ値が安定している場合は効果を出しやすいです。一方で、毎回新しく生成されるオブジェクトや配列、関数が props として渡されている場合、それだけで「変わった」と判断され、memo の意味が薄れます。つまり、memo を効かせたいなら、まず props 自体が安定している必要があります。

この点を理解せずに memo を乱用すると、「付けたのに変わらない」「むしろコードだけ複雑になった」という状態になりやすいです。重要なのは、memo を単独で考えないことです。props の設計、参照の安定性、親コンポーネントの構造まで含めて初めて意味を持ちます。つまり、memo は最後に足す装飾ではなく、props 設計と一緒に考えるべき最適化です。

4.2 重い子コンポーネントにだけ使う意識が大切

どの子コンポーネントにも一律で React.memo を付けると、かえってコードが読みにくくなり、最適化の意図も分かりにくくなります。比較コストが発生する以上、軽いコンポーネントでは効果がほとんどない場合もあります。つまり、memo は「すべての部品へ付けるもの」ではなく、再レンダリングを抑える価値がある場所へ限定して使ったほうがよいです。

たとえば、リスト項目が大量にある場合や、内部計算が重い描画コンポーネント、複雑な子ツリーを持つ部品などは memo の恩恵を受けやすいです。一方で、非常に軽量で props も頻繁に変わる部品では、あまり意味がないこともあります。つまり、memo は道具として便利ですが、「どこに効かせるべきか」を見極めて使うことが大切です。

import React from "react"; const UserCard = React.memo(function UserCard({ name, email }) {  console.log("render UserCard");  return (    <div>      <h3>{name}</h3>      <p>{email}</p>    </div>  ); }); export default function App() {  const [count, setCount] = React.useState(0);  return (    <div>      <button onClick={() => setCount(count + 1)}>count: {count}</button>      <UserCard name="Aki" email="[email protected]" />    </div>  ); }

この例では、Appcount が更新されても、UserCard の props は変わらないため、React.memo が効果を発揮しやすいです。親が再レンダリングされても、子の描画を抑えられる典型例です。

5. 関数 props が毎回新しくなると子は再レンダリングされやすい

React では、コンポーネントが再実行されるたびに、関数定義も新しく生成されます。そのため、親コンポーネントの中でそのまま書かれたイベントハンドラやコールバックを子へ props として渡していると、見た目には同じ処理であっても、参照としては毎回別物になります。この状態では、子コンポーネントが React.memo で包まれていても、「props が変わった」と判断されて再レンダリングされやすくなります。つまり、関数 props は無意識のうちに再レンダリングのきっかけを作りやすい要素です。

ただし、これも「すべての関数を useCallback で包めばよい」という話ではありません。軽い子コンポーネントや、そもそも親子ともにそれほど頻繁に更新されない構造であれば、関数参照の違いはほとんど問題にならないこともあります。重要なのは、関数 props の変化が実際に再レンダリングの原因になっているのかを見極めることです。そのうえで、必要な場所へだけ参照の安定化を入れるほうが、コードも読みやすく保ちやすいです。

5.1 useCallback は「何でも包む道具」ではない

useCallback は、関数参照を安定させるための便利なフックですが、これもまた万能ではありません。依存配列を適切に管理しなければ意味がありませんし、むやみに増やすとコードの読みやすさが下がることもあります。特に、子が memo されておらず、関数参照を安定させても再レンダリング削減に結びつかない場合には、効果が薄いこともあります。

つまり、useCallback は「React では必ず使うもの」ではなく、「関数 props の参照安定化が実際に価値を持つ場面」で初めて意味を持ちます。親の更新頻度、子の重さ、memo の有無とセットで考えるべきです。道具単体で考えると乱用しやすいですが、再レンダリングの流れの中で見れば、必要性はかなり判断しやすくなります。

5.2 子へ渡す callback が多い設計そのものも見直す

親から子へ多くの callback を渡している設計では、そもそも親がいろいろな責務を持ちすぎている可能性があります。入力、削除、更新、展開、保存など、あらゆる処理を親がハンドルし、その入口として大量の callback を渡していると、props の安定化以前に設計の複雑さが問題になります。これは再レンダリングの観点だけでなく、責務分離の観点でも見直し余地があります。

つまり、関数 props が多すぎると感じたら、「どの callback を安定させるか」だけでなく、「なぜこんなに多く渡しているのか」を考えるべきです。子側で完結できる責務、別コンポーネントへ分けられる責務、外へ出す必要がない責務を整理すると、結果として props も安定しやすくなります。

import React from "react"; const ActionButton = React.memo(function ActionButton({ onClick, label }) {  console.log("render ActionButton");  return <button onClick={onClick}>{label}</button>; }); export default function App() {  const [count, setCount] = React.useState(0);  const handleClick = React.useCallback(() => {    console.log("clicked");  }, []);  return (    <div>      <button onClick={() => setCount(count + 1)}>count: {count}</button>      <ActionButton onClick={handleClick} label="Open" />    </div>  ); }

この例では、handleClick の参照が安定しているため、ActionButtonlabel が変わらない限り再レンダリングされにくくなります。

6. オブジェクトや配列を props で毎回作ると参照が変わる

関数 props と同じように、オブジェクトや配列も、コンポーネントの再実行ごとに新しく作られれば参照が変わります。そのため、見た目には同じ内容でも、子コンポーネントから見ると「新しい props」が毎回渡されている状態になります。これは React.memo の shallow comparison に引っかかりやすく、不要な再レンダリングの原因になります。たとえば、style={{ color: "red" }} のようなインラインオブジェクトや、items={[1, 2, 3]} のような直接記述された配列は、毎回新しい参照になりやすい典型例です。

もちろん、こうした書き方が常に悪いわけではありません。子が軽く、更新頻度も低ければ、大きな問題にならないこともあります。しかし、再レンダリングを避けたい重い子コンポーネントへ渡している場合には、内容は同じなのに参照だけが変わることで最適化を無効にしてしまうことがあります。つまり、オブジェクトや配列の props は、「中身が同じかどうか」だけでなく「参照が安定しているかどうか」が重要です。

6.1 useMemo で安定化できるものは限定して考える

useMemo は、オブジェクトや配列の参照を安定させたいときに有効です。とくに、重い子コンポーネントへ渡す props や、計算コストの高い派生値に対しては意味を持ちます。しかし、これもまた memouseCallback と同じで、何でもかんでも包めばよいわけではありません。依存配列の管理が増え、読みやすさが下がるなら、かえってコストになることもあります。

大切なのは、「この値の再生成を避けることで、本当にメリットがあるか」を見ることです。毎回新しい配列を作ることそのものが問題なのではなく、それが子の不要な再レンダリングにつながっているときに初めて意味を持ちます。つまり、useMemo は「念のため付けるもの」ではなく、参照安定化や計算削減が実際に効く場所へ限定して使うべきです。

6.2 props のためだけに複雑な値を作らない

親コンポーネントの render 内で、子へ渡すためだけに複雑なオブジェクトを毎回組み立てていると、その props は当然不安定になります。これはとくに設定オブジェクトやフィルタ条件、表示オプションなどで起こりやすいです。見た目には便利でも、結果として子の memo を効きにくくしてしまいます。

そのため、props のためだけに複雑なオブジェクトを毎回作るより、子が本当に必要な最小限のプリミティブ値へ分解して渡したほうが自然な場合もあります。つまり、安定化のために useMemo へ頼るだけでなく、そもそも props の形を単純にできないかを見直すことも重要です。

import React from "react"; const Profile = React.memo(function Profile({ user }) {  console.log("render Profile");  return <div>{user.name}</div>; }); export default function App() {  const [count, setCount] = React.useState(0);  const user = React.useMemo(() => {    return { name: "Mika" };  }, []);  return (    <div>      <button onClick={() => setCount(count + 1)}>count: {count}</button>      <Profile user={user} />    </div>  ); }

この例では、user オブジェクトの参照が安定しているため、親の count が変わっても Profile は不要に再レンダリングされにくくなります。

7. 計算コストの高い派生値は useMemo で分離する

不要な再レンダリングというと、ついコンポーネントそのものの再実行ばかりに目が向きますが、実際には「再レンダリングのたびに毎回重い計算をしている」ことも問題になります。たとえば、一覧データのフィルタリング、ソート、集計、マッピング、複雑な変換処理などは、描画そのものよりも計算コストが大きいことがあります。UI が変わっていなくても、親コンポーネントの再実行に引きずられて毎回これらが再計算されるなら、体感性能へ十分影響します。つまり、不要な再レンダリングを避けるには、描画回数だけでなく、その中で毎回走る計算も見直す必要があります。

このとき有効なのが useMemo です。依存が変わったときだけ派生値を再計算し、それ以外では前回の結果を再利用できるため、重い処理の繰り返しを抑えやすくなります。ただし、ここでも重要なのは「すべてを memo 化する」のではなく、「計算コストが無視できず、依存関係も明確なもの」に限定して使うことです。つまり、useMemo は描画を止める道具というより、再レンダリングのたびに走る無駄な計算を減らすための道具として理解したほうが自然です。

7.1 フィルタ・ソート・集計は再計算の対象になりやすい

大量データのフィルタリングやソートは、見た目以上にコストが高いことがあります。とくに検索一覧、商品一覧、ダッシュボードのような画面では、少しの state 更新のたびに同じフィルタやソートを再実行していることがあります。UI 上の変化が小さくても、内部ではかなり重い処理が繰り返されているケースは珍しくありません。

そのため、こうした派生値は「描画のついで」に計算するのではなく、依存が変わったときだけ再評価されるよう分離して考えたほうがよいです。useMemo を使うことで、不要な再計算を減らし、再レンダリング時の負荷をかなり抑えられる場合があります。つまり、重い一覧処理は描画と一体化させず、派生値として明確に切り出すことが重要です。

7.2 useMemo は重い計算に絞るほうが読みやすい

useMemo は便利ですが、軽い計算にまで広く適用しすぎると、逆にコードの意図が分かりにくくなります。単純な結合や短い変換程度なら、毎回再計算しても問題にならないことが多いです。無理に memo 化すると、依存配列の管理だけが増え、むしろ保守性を下げることもあります。

つまり、useMemo は「何でも最適化するための記号」ではなく、「ここは計算が重く、依存もはっきりしている」と説明できる場所へ使うのが理想です。必要な場所へ絞って使うと、コードも読みやすく、最適化の意図も伝わりやすくなります。

import React from "react"; export default function ProductList({ products, keyword }) {  const filteredProducts = React.useMemo(() => {    return products.filter((product) =>      product.name.toLowerCase().includes(keyword.toLowerCase())    );  }, [products, keyword]);  return (    <ul>      {filteredProducts.map((product) => (        <li key={product.id}>{product.name}</li>      ))}    </ul>  ); }

この例では、products または keyword が変わったときだけフィルタ処理が再実行されます。親の他の更新による不要な再計算を防ぎやすくなります。

8. Context は便利だが、広く使いすぎると再レンダリングが増えやすい

Context は、深い props drilling を避けられる便利な仕組みです。テーマ、認証情報、ユーザー設定、アプリ全体の共通状態などを共有するときには非常に有効です。しかし、その便利さゆえに、何でも Context へ入れてしまうと、不要な再レンダリングの原因になりやすくなります。Context の値が変わると、その Context を参照しているコンポーネント群は広く再評価されるため、少しの変更でも大きな範囲を巻き込みやすいからです。つまり、Context は state の共有を楽にする一方で、更新範囲も広げやすい道具です。

とくに問題なのは、更新頻度の高い値と低い値を同じ Context にまとめてしまうことです。たとえば、ユーザーのテーマ設定のようにほとんど変わらない値と、リアルタイムな入力状態のようによく変わる値を一緒に入れてしまうと、頻繁な更新のたびに広い範囲が再レンダリングされやすくなります。そのため、Context の最適化では、「使うかどうか」だけでなく、「何をどの粒度で共有するか」が非常に重要です。

8.1 変化の頻度が違う値を同じ Context に入れない

Context にまとめる値は、意味が近いことだけでなく、更新頻度もある程度そろっているほうが扱いやすいです。頻繁に変わる値と、ほとんど固定の値を同じ Context へ入れると、小さな変化で広い範囲の参照先が再評価されやすくなります。これは、Context が便利であるがゆえに起きやすい設計上の落とし穴です。

そのため、Context は「共有したいものを何でも入れる箱」としてではなく、更新単位を意識して分割するほうが安定します。たとえば、テーマ用 Context、認証用 Context、入力状態用 Context を分けるだけでも、更新波及をかなり抑えられることがあります。つまり、Context 最適化の第一歩は、値の意味よりも更新のまとまりを意識することです。

8.2 Context の value 参照も安定させる

Context Provider の value に毎回新しいオブジェクトを直接渡していると、それだけで参照が変わり、利用側コンポーネントが再レンダリングされやすくなります。たとえば、value={{ user, theme }} のような書き方は便利ですが、親の再レンダリングのたびに新しいオブジェクトになるため、意図しない更新を招きやすいです。

この問題は useMemo を使って value を安定させることである程度防げます。ただし、ここでも重要なのは、単にメモ化することではなく、何を同じ Context へ入れるのか自体を見直すことです。つまり、Context の最適化は value の書き方だけでなく、共有粒度の設計とセットで考えるべきです。

import React from "react"; const ThemeContext = React.createContext(null); export function ThemeProvider({ theme, children }) {  const value = React.useMemo(() => ({ theme }), [theme]);  return (    <ThemeContext.Provider value={value}>      {children}    </ThemeContext.Provider>  ); }

このように value を安定させておくと、親が別の理由で再レンダリングしても、theme が変わらない限り不要な更新を抑えやすくなります。

9. リスト描画では item 単位の設計が重要になる

不要な再レンダリングが目立ちやすい典型的な場面の一つが、リスト描画です。大量のアイテムが並ぶ UI では、親コンポーネントの小さな更新だけで、全アイテムが毎回再レンダリングされるような構造になっていることがあります。表示上は一部の項目しか変わっていないのに、全体が再実行されると、体感性能もかなり落ちやすくなります。とくにチャット、商品一覧、管理画面のテーブル、ファイル一覧のように行数が多い画面では、この差が非常に大きくなります。

リスト最適化で重要なのは、「一覧全体をどう描くか」よりも、「各 item をどれだけ独立した更新単位として扱えるか」です。親の state 変更に引きずられて全 item が再描画される構造では、いくら一部を memo 化しても根本的な改善がしにくいことがあります。item ごとの props、key、handler、派生値の持ち方まで含めて設計する必要があります。つまり、リスト描画最適化は list コンポーネント単体の問題ではなく、item 設計の問題でもあります。

9.1 key が安定していないと更新効率が落ちる

React における key は、単なる warning 回避のためのものではありません。どの要素が同一のものかを識別するための重要な手がかりです。もしインデックスを安易に使ったり、不安定な値を使ったりすると、並び替えや追加・削除のたびに意図しない再利用や再生成が起きやすくなります。その結果、状態保持や差分更新の効率も悪くなり、再レンダリングの最適化が効きにくくなることがあります。

そのため、リスト item には、データそのものに紐づく安定した一意キーを使うべきです。これは再レンダリング削減だけでなく、UI の整合性を守るためにも重要です。つまり、key は小さな記述に見えて、一覧の更新効率と安定性を大きく左右する設計要素です。

9.2 item コンポーネントを分離して memo しやすくする

リスト描画を parent の render 内にすべて書いていると、親が更新されるたびに各 item のレンダリングロジックまで再実行されやすくなります。これでは一覧が大きいほどコストが増えやすく、どこを最適化すればよいのかも見えにくくなります。item 単位でコンポーネントを切り出し、その props を安定させたうえで memo を使うほうが、更新範囲を管理しやすくなります。

もちろん、ただ分ければよいわけではなく、item に渡す props の形や handler の参照安定性も重要です。しかし、少なくとも item コンポーネントを独立させておけば、「どの項目が本当に変わったのか」を React 側が判断しやすくなります。つまり、リスト最適化では、一覧全体を 1 つの大きな render として扱わず、item ごとに責務を切り分けることが効果的です。

import React from "react"; const TodoItem = React.memo(function TodoItem({ todo }) {  console.log("render", todo.id);  return <li>{todo.text}</li>; }); export default function TodoList({ todos }) {  return (    <ul>      {todos.map((todo) => (        <TodoItem key={todo.id} todo={todo} />      ))}    </ul>  ); }

この形にしておくと、todo の参照が安定している限り、一覧全体の再描画が即すべての item 再描画につながるとは限らなくなります。

10. useEffect 内の state 更新が連鎖すると無駄な render を生みやすい

不要な再レンダリングは、props や state の渡し方だけでなく、useEffect の書き方からも生まれます。とくに多いのは、「render → effect 実行 → state 更新 → 再 render」という流れを何度も引き起こすパターンです。もちろん、effect で state を更新すること自体が悪いわけではありません。しかし、描画後に毎回走る effect の中で、さらに state を変えていると、それだけで追加の再レンダリングを発生させる構造になります。必要な同期処理なら問題ありませんが、本来 render 中に計算できるものや、別の責務として設計できるものまで effect に入れてしまうと、無駄な更新が増えやすいです。

さらに、依存配列の設計が曖昧だと、この問題は悪化しやすくなります。effect が毎回走り、そのたびに state が少しずつ変わり、結果として再レンダリングが連鎖する状態は、パフォーマンスだけでなく挙動の予測可能性も下げます。つまり、不要な再レンダリングを減らしたいなら、useEffect を「何でも後処理を書く場所」にしないことが大切です。effect は本当に副作用が必要な場面へ限定し、単なる派生値や同期可能なロジックまで抱え込ませないほうが自然です。

10.1 render 中に求められる値を effect で作らない

props や state から素直に導ける値を、わざわざ useEffect で state に入れ直す構造は、不要な render を増やしやすいです。たとえば、fullName のような単純な派生値や、フィルタ済み配列のような計算結果を effect で別 state に同期していると、「元の値が変わる → render → effect → setState → 再 render」という余計な 1 周が発生します。本来 1 回で済む更新を、わざわざ 2 回にしているようなものです。

このような値は、render 中に計算するか、必要なら useMemo で扱うほうが自然です。effect は、外部 API との同期や DOM 操作のような、本当に副作用が必要な場面へ限定したほうが分かりやすくなります。つまり、不要な再レンダリングを減らすには、effect を「計算の置き場」にしないことも重要です。

10.2 依存配列の不安定さが無駄な effect 実行を生む

effect の依存配列に毎回新しい関数やオブジェクトが入っていると、それだけで effect が繰り返し実行されやすくなります。そして、その effect 内で state 更新が行われていれば、無駄な render の連鎖を生みやすいです。この問題は、props の参照不安定性とも深くつながっています。

そのため、effect 最適化では「この effect は本当に必要か」に加えて、「依存が安定しているか」も見る必要があります。効果が必要な場合でも、依存の持ち方が不安定なら、設計全体を見直したほうがよいことがあります。つまり、effect は副作用のための仕組みですが、そのまわりの参照設計まで含めて安定していなければ、不要な再レンダリングの温床になりやすいです。

11. 計測しない最適化は当たり外れが大きい

不要な再レンダリングを減らしたいとき、感覚だけで「ここが重そうだ」と決めつけて最適化を始めると、あまり効果が出ないことがあります。たとえば、実際には軽いコンポーネントに memo をたくさん付けてしまったり、本当の原因が Context や effect の連鎖にあるのに、関数参照の安定化ばかり頑張ってしまったりすることがあります。つまり、再レンダリング最適化は、思い込みだけで進めると効率が悪くなりやすいです。

だからこそ、計測が重要になります。React DevTools の Profiler を使えば、どのコンポーネントが何回 render されているのか、どの更新でどこが再描画されたのかをかなり具体的に見られます。どこが本当にボトルネックなのかが見えると、memouseMemo、Context 分割、state 粒度見直しのどれを先にやるべきかも判断しやすくなります。つまり、「何となく最適化する」のではなく、「計測して、理由を持って最適化する」ことが大切です。

11.1 React DevTools Profiler を使う意味

Profiler は、単に render 回数を数えるための道具ではありません。どの interaction に対してどのコンポーネントが再評価されたのか、その更新がどれくらい時間を使っているのかを見ることで、「不要な render がどこにあるか」を把握しやすくなります。見た目には問題なさそうでも、実際には同じ部分が何度も描画されていることもあります。

また、Profiler を使うと、感覚では重そうに見えた場所が実は軽く、本当に重いのは別の場所だったというケースにも気づきやすくなります。つまり、計測は最適化のための前提であり、無駄な努力を減らすためにも欠かせません。

11.2 「render 回数」だけでなく「重さ」も見る

再レンダリングが多いことと、それが問題であることは必ずしも一致しません。軽いコンポーネントが何度か再評価されるだけなら、実害はほとんどないこともあります。一方で、回数は少なくても、重い計算や複雑な子ツリーを含む render なら、そちらのほうが深刻です。

そのため、render 回数だけを見て機械的に減らすのではなく、「何がどれだけ重いのか」も一緒に見る必要があります。つまり、最適化では「多いから悪い」ではなく、「コストに対して不要かどうか」で判断することが重要です。

12. 不要な再レンダリング対策は「道具」より「設計」で決まる

ここまで見てきたように、不要な再レンダリングを避ける方法はいくつもあります。React.memouseMemouseCallback、Context 分割、state 粒度調整、コンポーネント分割、effect 見直しなど、個別のテクニックはいずれも有効です。しかし、本当に大切なのは、それらを単発の最適化テクニックとして覚えることではありません。なぜなら、不要な再レンダリングの多くは、ツール不足からではなく、設計の曖昧さから生まれるからです。state の責務、props の形、親子関係、共有範囲、更新境界が整理されていれば、そもそも過剰な render は起きにくくなります。

逆に、設計が曖昧なまま memouseCallback を積み上げると、たしかに一部は改善しても、全体としては読みにくくメンテナンスしづらいコードになりやすいです。つまり、不要な再レンダリング対策は「どの道具を使うか」より、「どんな構造にするか」で決まる部分が大きいです。ローカルに閉じるべき state は閉じる、親が何でも抱えない、props を安定させる、共有範囲を広げすぎない、effect を副作用へ限定する、といった基本の設計ができていれば、最適化テクニックは補助として自然に効いてきます。最終的には、レンダリングの回数を減らすことそのものではなく、必要な更新だけが自然に起きる構造を作ること が、最も強い最適化になります。

おわりに

不要な再レンダリングを避けるためのアプローチは、単に memouseMemo といったフックの使い方を覚えることにとどまりません。本質的には、state の粒度をどこまで細かく分割するか、コンポーネントの親子構造をどう設計するか、props の安定性をどのように担保するか、関数参照をどこで固定するか、派生値の計算をどこに置くかといった複数の要素を一体として見直す必要があります。さらに、Context の共有範囲が適切か、リストレンダリングの設計に無駄がないか、effect の責務が肥大化していないか、そして実際にどこで無駄な再描画が起きているかを計測する習慣まで含めて、初めて実践的な最適化といえます。つまり、再レンダリング最適化とは個別のテクニックの積み重ねではなく、コンポーネント設計そのものを整える行為です。

また、再レンダリングは React における正常な挙動であり、完全に排除すべき対象ではありません。問題になるのは、その影響範囲が意図せず広がり、関係のない部分まで巻き込んでしまうケースです。このような状態では、パフォーマンスの低下だけでなく、どこで何が更新されているのかが把握しづらくなり、結果として保守性や予測可能性も損なわれていきます。そのため重要なのは、「再レンダリングを減らす」こと自体ではなく、「必要な更新だけが自然に伝播する構造」を作ることにあります。

そのためには、state をどこに持たせるかという配置の設計が出発点になります。状態を過剰に持ち上げれば広範囲に再レンダリングが波及しやすくなり、逆に分散しすぎると整合性の維持が難しくなります。また、props の設計においても、頻繁に変わる値と安定させたい値を意識的に分離することで、不要な更新の伝播を抑えることができます。さらに、共有すべき情報とローカルに閉じるべき情報を見極めることで、Context の過剰な再評価を防ぎ、構造としての安定性を高めることができます。

このように考えると、最適化は後から付け足す“魔法”のようなものではなく、設計の初期段階から織り込むべき性質であることがわかります。レンダリングの流れを意識した構造を最初から作ることで、結果としてコードは自然に軽くなり、意図しない副作用も減っていきます。そしてその状態は、単に高速であるだけでなく、読みやすく、変更に強く、チームで扱いやすいコードベースへとつながっていきます。

LINE Chat