ReactにおけるSignals:大規模アプリケーションの再レンダリングを最適化する「最終兵器」

Development tutorial - IT technology blog
Development tutorial - IT technology blog

再レンダリングの悩みとSignalsによる解決策

Reactプロジェクトが大規模になるほど、再レンダリングによるパフォーマンス低下の苦労が身に染みるようになります。根本的な問題はReactの更新メカニズムにあります。親コンポーネントでuseStateを変更すると、子コンポーネントがそのデータを使っていなくても、コンポーネントツリー全体が再レンダリングの渦に巻き込まれてしまうことがよくあります。

私が以前携わった株価ダッシュボード의プロジェクトを例に挙げます。500以上の銘柄が毎秒更新される環境で、useStateとContext APIを組み合わせて使用したところ、ブラウザのCPU使用率は常に80〜90%に達していました。FPSは15〜20まで落ち込み、明らかなカクつきが発生していました。そこでSignalsへの移行を提案したところ、FPSは安定して60を維持し、スムーズに動作するようになりました。5人の開発チームも、不要なレンダリングを防ぐためにuseMemouseCallbackを張り巡らせる必要がなくなり、胸をなでおろしました。

SignalsはSolidJSやPreactですでに有名なため、決して新しい概念ではありません。しかし、Reactに導入されると、ゲームチェンジャーとなります。コンポーネント全体を再レンダリングする代わりに、SignalsはDOM上の表示位置の値を直接更新します。巨大なコンポーネントツリー全体を動かすことなく、画面上の特定の数字だけを書き換える様子を想像してみてください。

プロジェクトへのSignalsの導入

まずは、@preact/signals-reactライブラリを使用します。これは現在、ReactエコシステムにSignalの仕組みを取り入れるための最も安定した解決策です。ターミナルを開き、以下のコマンドを実行します。

npm install @preact/signals-react

ViteやNext.jsを使用している方への注意点:最高のパフォーマンスを発揮し、変更を自動的に追跡するには、Babelプラグインの追加設定が推奨されます。ただし、まずはライブラリをインストールするだけで、その違いを十分に体感できるはずです。

コードでの実践的な実装

1. Signal의 初期化

useStateの代わりに、signal()関数を使用します。特筆すべき点は、コンポーネントの外で宣言できるため、非常に軽量なグローバルステート(Global State)として利用できることです。

import { signal } from "@preact/signals-react";

// コンポーネント外で宣言し、どのファイルからでもアクセス可能
const count = signal(0);

function Counter() {
  return (
    <div>
      <p>クリック回数: {count.value}</p>
      <button onClick={() => count.value++}>カウントアップ</button>
    </div>
  );
}

2. Computed Signalsによる最適化

ある値が他のSignalに依存している場合は、computedを使用します。これはuseMemoに似ていますが、よりスマートです。元のデータが実際に変更されたときにのみ再計算を行います。

import { signal, computed } from "@preact/signals-react";

const cartItems = signal([{ id: 1, price: 150000 }, { id: 2, price: 200000 }]);

// カートが更新されるたびに合計金額を自動計算
const totalPrice = computed(() => 
  cartItems.value.reduce((acc, item) => acc + item.price, 0)
);

function ShoppingCart() {
  return <h2>合計金額: {totalPrice.value} VNĐ</h2>;
}

3. effectによるサイドエフェクトの管理

effect()関数はuseEffectに代わるものです。内部で使用しているSignalを自動的に検知して再実行するため、依存関係配列 [] を記述する必要はありません。

import { signal, effect } from "@preact/signals-react";

const theme = signal("dark");

effect(() => {
  // テーマの変更をログ出力
  console.log(`テーマが変更されました: ${theme.value}`);
  document.body.className = theme.value;
});

実際のパフォーマンス検証

Signalsが本当に効果的かどうかを確認するには、React DevToolsのProfilerを開いてみてください。

useStateでは、ステートが変わるたびにコンポーネントが「着色」され、再レンダリングが通知されます。一方、Signalsでcount.valueを変更しても、それを含むコンポーネントは完全に静止したままです。SignalsはDOM内のテキストノードを直接操作し、Reactの重いVirtual DOMの差分抽出(Diffing)プロセスをスキップするからです。

大規模な状態管理のコツ

  • ファイル構成: Signalsはstore/ディレクトリにまとめましょう。メンテナンス性を高めるため、authSignal.jscartSignal.jsのように個別に分割するのがベストです。
  • 適材適所: useStateを完全に捨てる必要はありません。シンプルなフォームや、一つのコンポーネント内だけで完結するステートには、依然としてuseStateが軽量な選択肢となります。
  • ループの回避: Signal AがSignal Bを更新し、それがまたAに戻るような無限ループを防ぐため、effect内のロジックは慎重に設計してください。

大規模なアプリケーションでレンダリングのエラーを追跡するのは非常に時間がかかります。Signalsはデータロジックをコンポーネントのライフサイクルから切り離すことで、コードの可読性とデバッグのしやすさを大幅に向上させます。もしプロジェクトのパフォーマンスに課題を感じているなら、まずは小さなモジュールからSignalsを導入してみてください。その違いをすぐに実感できるはずです。

Share: