React Context APIのパフォーマンス最適化を徹底解説!useMemoで無駄な描画を防ぐ方法
生徒
「Reactのコンテキストを使ってみたんですけど、一箇所の数字を変えただけで画面全体が何度も作り直されているみたいで、動きがカクカクします。これって直せますか?」
先生
「それはパフォーマンスの低下が起きていますね。Reactにはユーズメモ(useMemo)という『計算結果を記憶する魔法』があります。これを使えば、無駄な作業を省いてサクサク動くようになりますよ。」
生徒
「ユーズメモ…難しそうですが、初心者の私でも使いこなせますか?」
先生
「大丈夫です。仕組みを身近な例えでお話ししますね。まずは、なぜ無駄な動きが発生するのか、その原因から見ていきましょう!」
1. Reactのレンダリングとコンテキストの悩み
Reactという道具を使ってWebサイトを作るとき、一番大切な動きが「レンダリング」です。レンダリングとは、画面のデータが書き換わったときに、新しい見た目を作り直して表示する作業のことです。例えば、ボタンを押して数字が1から2に変わったとき、Reactは画面を書き換えてくれます。
しかし、Context API(コンテキストエーピーアイ)という「共有の倉庫」を使っていると、困ったことが起こります。この倉庫の中身が少しでも変わると、その倉庫に関係している部品がすべて「念のため作り直そう!」と動き出してしまいます。本当は書き換える必要がない部品まで一緒に作り直されるため、これが積み重なるとパソコンやスマホの動作が重くなってしまうのです。これを解決するために、無駄な作業をさせない工夫が必要になります。
2. useMemoは「計算結果のメモ帳」
ここで登場するのが useMemo(ユーズメモ)です。これは簡単に言うと「結果を書き留めておくメモ帳」のようなものです。例えば、非常に複雑な計算問題があるとします。毎回ゼロから計算し直すのは大変ですが、一度計算した答えをメモ帳に書いておけば、次からはそのメモを見るだけで済みますよね。
Reactの世界でも同じです。前回の計算結果と、その計算に使う材料(依存配列と呼びます)を比べて、材料が変わっていなければ「計算し直さずに前の結果をそのまま使うよ!」とReactに教えてあげることができます。これが「パフォーマンス最適化」と呼ばれる、サイトを軽くするための重要な技術です。これを使うことで、コンテキストの倉庫が更新されても、特定の部品だけは以前の状態を保つことができるようになります。
3. useMemoを使わない場合の重い処理
まずは、最適化をしていない「重くなりやすいコード」を見てみましょう。この例では、ユーザー情報(名前)と、全く関係のないカウンター(数字)を同じ場所で管理しています。数字を増やすだけで、名前の表示部分まで不必要に反応してしまう状態です。
import React, { useState, createContext } from "react";
export const UserContext = createContext();
export function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("たろう");
// ここで渡すデータが、毎回新しく作られてしまいます
const value = { name, setName };
return (
<UserContext.Provider value={value}>
<div>
<button onClick={() => setCount(count + 1)}>
数字を増やす(現在:{count})
</button>
<UserNameDisplay />
</div>
</UserContext.Provider>
);
}
function UserNameDisplay() {
console.log("名前の部品が再描画されました");
return <div>ユーザー名:たろう</div>;
}
4. useMemoを導入して無駄を防ぐ書き方
それでは、先ほどの無駄を useMemo で解消してみましょう。書き方はとてもシンプルです。 `useMemo(() => 結果, [材料])` という形の中に、保存しておきたいデータを入れます。こうすることで、名前が変わったときだけ新しいデータを作り、数字が変わったときは前のデータを使い回すようになります。
プログラミング未経験の方は、「もし材料(名前)が変わっていなければ、前回の結果をそのまま使う」という約束を交わしているのだと考えてください。これにより、余計な部品の作り直しをピタリと止めることができます。
import React, { useState, createContext, useMemo } from "react";
export const UserContext = createContext();
export function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("たろう");
// nameが変わったときだけ、新しいオブジェクトを作成します
const value = useMemo(() => ({
name,
setName
}), [name]); // ここに書いたものが変わらない限り、結果を再利用!
return (
<UserContext.Provider value={value}>
<button onClick={() => setCount(count + 1)}>
カウント:{count}
</button>
<UserNameDisplay />
</UserContext.Provider>
);
}
5. 依存配列(材料リスト)を正しく設定しよう
useMemoを使うときに一番気をつけなければならないのが、後ろにある `[]` の中身です。これは「依存配列(いぞんはいれつ)」と呼ばれます。ここには、計算に使うすべての変数を入れなければなりません。もし、必要な変数を入れ忘れてしまうと、データが変わったのに画面が古いまま、という不具合(バグ)が起きてしまいます。
例えば、名前だけでなく年齢も表示したい場合は、配列の中に名前と年齢の両方を入れます。こうすることで、「名前か年齢、どちらかが変わったら新しく作り直してね」という指示になります。逆に、何も入れたくない場合は空の配列 `[]` を使います。これは「最初の一回だけ計算して、あとはずっと同じものを使って」という意味になります。
6. オブジェクトの参照と同一性チェック
少し難しいお話をしますが、パソコンは「内容が同じ」であることと「同じ場所にあるデータ」であることを区別しています。例えば、中身が全く同じ二つのノートがあっても、それは別のノートですよね。JavaScript(ジャバスクリプト)という言語では、新しくデータ(オブジェクト)を作るたびに、内容が同じでも「新しい別のノート」として扱われます。
Reactのコンテキストは、この「新しいノートになったかどうか」をチェックして再描画を決めます。useMemoを使わないと、毎回「新しいノート」を渡してしまうため、中身が同じでもReactは「あ、新しいノートだ!作り直さなきゃ!」と勘違いしてしまいます。useMemoは「同じノートをずっと使い続ける」ためのテクニックなのです。
// 悪い例:レンダリングのたびに新しい「箱」が作られる
const value = { theme: "dark" };
// 良い例:一度作った「箱」を大切に使い回す
const value = useMemo(() => ({ theme: "dark" }), []);
7. いつuseMemoを使うべきか
「じゃあ全部の場所にuseMemoを使えばいいの?」と思うかもしれませんが、実はそうではありません。メモを取ること自体にも、ほんの少しだけ手間(コスト)がかかるからです。メモ帳を用意して、以前のものと比較して…という作業を、あまりにも単純な場所で行うと、かえって効率が悪くなることもあります。
使うべき目安は、以下の二つのパターンです。一つは「計算にすごく時間がかかる処理」をするとき。もう一つは、今回のコンテキストのように「そのデータがたくさんの部品に配られるとき」です。特にコンテキストの `value` に渡すデータは、多くの部品に影響を与えるため、積極的に useMemo を使って最適化するのがプロの現場での常識となっています。
8. コンテキスト最適化の仕上げ:複数の値を渡すとき
実際の開発では、一つのコンテキストで「今の設定値」と「設定を変える関数」の両方を渡すことが多いです。このとき、二つをまとめて一つのオブジェクト(情報の塊)にすることが一般的ですが、ここが最も最適化が必要なポイントになります。以下の完成形を参考に、サクサク動くアプリの土台を作ってみましょう。
import React, { createContext, useState, useMemo, useContext } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(false);
// 切り替え関数と状態を一つのオブジェクトにまとめ、useMemoで保護する
const themeData = useMemo(() => ({
isDark,
toggleTheme: () => setIsDark(prev => !prev)
}), [isDark]); // isDarkが変わったときだけ新しくする
return (
<ThemeContext.Provider value={themeData}>
{children}
</ThemeContext.Provider>
);
}
// 使うときは、useContextを呼び出すだけ!
function ThemeButton() {
const { isDark, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
{isDark ? "明るいモードへ" : "暗いモードへ"}
</button>
);
}