【テクニカル・上級編】 遅延初期化(Lazy Initialization) – React実践ガイド

useStateの裏側で何が起きているか?計算コストの罠と「遅延初期化」という名の防衛策

こんにちは。日夜、DOMのツリー構造とJavaScriptのヒープメモリの狭間でパフォーマンストラブルの火消しに追われているフロントエンド・アーキテクチャ担当の者です。

Reactの開発において、最も頻繁に召喚されるHooksといえば間違いなく `useState` です。しかし、この一見無害でシンプルなフックの「初期値の渡し方」を一つ誤るだけで、アプリケーションのレンダリングパイプラインに深刻なフリーズを生み出したり、不必要なCPUサイクルの浪費を招いたりすることをご存知でしょうか。

今回は、多くのシニアエンジニアが一度はハマる「初期値の計算コスト」と、それを美しく、かつ完璧にいなすための奥義「遅延初期化(Lazy Initialization)」について、Reactの内部挙動のメタファーを交えながら徹底的に深掘りしていきます。

—

1. 惨劇は「毎回の再描画」でひっそりと起きる

まずは、以下のコードを見てください。一見して、何の変哲もないユーザー設定を初期化するコンポーネントです。

import React, { useState } from ‘react’;

// 例:重い処理や、localStorageへの同期的アクセスを模した関数
const calculateInitialSettings = () => {
console.log(‘【警告】重い初期化処理が実行されました!’);
let data = {};
try {
// ローカルストレージからのパース(データ量が多いとメインスレッドをブロックする)
const stored = localStorage.getItem(‘app_heavy_settings’);
data = stored ? JSON.parse(stored) : { theme: ‘dark’, version: 1 };
} catch (e) {
data = { theme: ‘dark’, version: 1 };
}
return data;
};

export const UserDashboard: React.FC = () => {
// アンチパターン:直接関数を呼び出して結果を渡している
const [settings, setSettings] = useState(calculateInitialSettings());
const [count, setCount] = useState(0);

return (

ユーザーダッシュボード

現在のカウント: {count}

{/ ボタンを押すたびに、再レンダリングが発生する /}

);
};

さて、このコンポーネントで「再描画トリガー」ボタンを何度かポチポチと押してみてください。
ブラウザのコンソールを開いていると、恐ろしいことに気づくはずです。

ボタンを押して `count` ステートが更新され、コンポーネントが再描画(Re-render)されるたびに `calculateInitialSettings()` が実行され、コンソールにログが出力されているではありませんか。

なぜ、初期化関数が毎回走るのか?

JavaScriptの言語仕様として、関数コンポーネントの本体が実行されるとき、JSX内の式や引数は毎回のレンダー毎に評価(Evaluation)されます。

// この行は、コンポーネントが再描画されるたびに評価される
useState(calculateInitialSettings());

Reactは賢いので、2回目以降のレンダーにおいて `useState` に渡された引数(この場合は `calculateInitialSettings()` の戻り値)を無視し、内部ですでに保持しているステートの値を使います。しかし、それは「値が使われない」だけであって、「引数として渡すための関数呼び出し自体」は毎レンダー容赦なく実行されているのです。

もしこの初期化処理が、数千件のJSONパース、複雑なアルゴリズムによるデータ構造の構築、あるいは同期的で重いストレージアクセスを含んでいたらどうなるでしょうか?
ユーザーが画面内でちょっとしたインタラクション(入力やトグルなど)を行うたびに、メインスレッドが微小にブロックされ、フレームレート(FPS)が低下する「カクつき」の原因になります。これが、私たちが日々戦っているパフォーマンス劣化の正体です。

—

2. 救世主:遅延初期化(Lazy Initialization)のメカニズム

この問題を一撃で解決するのが、Reactの `useState` が標準で備えている「関数を引数に取るオーバーロード(遅延初期化)」という機能です。

書き方は極めてシンプル。初期値として「値をそのまま渡す」のではなく、「初期値を返す関数(getter)」そのものを渡すだけです。

// 修正前(アンチパターン)
const [settings, setSettings] = useState(calculateInitialSettings());

// 修正後(遅延初期化)
// 括弧(())をつけて実行するのではなく、関数への参照を渡す
const [settings, setSettings] = useState(() => calculateInitialSettings());

Reactの内部で何が起きているのか?

この関数形式(`() => calculateInitialSettings()`)を渡した場合、Reactのファイバー(Fiber)アーキテクチャの動きは劇的に変わります。

1. 初回マウント時(Mount):
Reactは、コンポーネントの初回レンダリング時にのみ、渡された関数を一度だけ実行(遅延評価)し、その戻り値を初期ステートとしてメモリ上にスロット(Hookインデックス)として確保します。
2. 2回目以降の再描画時(Update):
Reactはコンポーネントが再描画される際、ステートの初期化フェーズを完全にスキップします。当然、アロー関数自体が再評価されることはありません(※インライン関数として記述しているため毎レンダー関数のインスタンスは生成されますが、Reactの内部スケジューラがそれを実行・評価することはありません)。

これにより、初回のマウントコストを必要な瞬間まで先送り(Lazy)しつつ、無駄なCPUサイクルの消費を完全に断ち切ることができます。

—

3. 実務で遭遇する「本当に危ない」遅延初期化のユースケース

単なる `localStorage` の読み込みだけではありません。シニアエンジニアとして実務のコードレビューをしていると、以下のようなケースで遅延初期化が「必須の防衛策」となります。

パターンA:重い初期設定オブジェクトのディープクローンや不変データ構造の生成

Immutable.js や複雑なモデリングクラスのインスタンスを初期値に置く場合、毎レンダー生成コストがかかるため、遅延初期化は必須です。

パターンB:URLクエリパラメータやwindowオブジェクトへの依存

SSR(Server-Side Rendering)環境において、`window` や `document`、`location` といったブラウザグローバルオブジェクトに依存する初期値を安全に取得する場合です。

import { useState } from ‘react’;

export const useWindowWidth = () => {
// SSR時に window が存在しない環境(Next.jsのサーバーサイド等)でのクラッシュを防ぎつつ、
// クライアントサイドの初回マウント時のみ安全に window のサイズを取得する
const [windowWidth, setWindowWidth] = useState(() => {
if (typeof window === ‘undefined’) {
return 1200; // デフォルトのフォールバック値
}
console.log(‘初回のみ:Window幅を計測中…’);
return window.innerWidth;
});

// … 継続的なリサイズリスナーの登録などの処理
return windowWidth;
};

もしこれを通常の `useState(window.innerWidth)` で書いていると、SSRのコンパイルやハイドレーション周りで思わぬバグや、ハイドレーションミスマッチの温床になることがあります。遅延初期化は、環境の境界線を安全にまたぐためのテクニックとしても非常に強力です。

—

4. アーキテクトからの提言:使うべき境界線を見極めよ

すべての `useState` にアロー関数を噛ませればいいかというと、そうではありません。ここがエンジニアの腕の見せ所です。

  • 遅延初期化を使うべきケース:
  • 配列やオブジェクトのリテラル生成であっても、データ量が膨大でCPUコストがかかる場合。
  • `localStorage`, `sessionStorage`, `IndexedDB` への同期アクセス。
  • 複雑な計算アルゴリズム、DOMの計測(`getBoundingClientRect` など)。
  • 外部環境(`window` やランダム値生成など)に依存する初期値。
  • 通常の直接指定で十分なケース:
  • プリミティブ値(数値を表す `useState(0)`、真偽値の `useState(false)`、短い文字列など)。これらはJavaScriptのエンジンにとって生成コストがゼロに等しいため、遅延初期化の構文を書くことによるコードの可読性低下のデメリットの方が大きくなります。

—

まとめ

パフォーマンスチューニングとは、派手なメモ化(`useMemo` や `useCallback`)を闇雲にばらまくことではありません。それよりも、Reactのレンダリングライフサイクルを正しく理解し、「どこで無駄なコストが発生しているのか」をピンポイントで潰していくことこそが本質です。

`useState` の遅延初期化は、ほんの数文字の書き方(`() => …`)の違いですが、アプリケーションが成長し、コードベースが巨大になったときに、確実にその真価を発揮します。

今夜、あなたのプロジェクトのコードベースを開き、`useState(` の中身を覗いてみてください。不要な計算をその都度走らせている爆弾が、しれっと眠っていないでしょうか?

堅牢でスケーラブルなフロントエンドを築くための一歩は、こうした細部の徹底的な最適化の積み重ねにほかならないのです。それでは、良きReactライフを。

コメント

タイトルとURLをコピーしました