2026年React状態管理選択ガイド: Zustand vs Jotai vs Signals の性能比較と不要な再レンダリング防止

Reactエコシステムにおける状態管理ライブラリの選定基準は、かつての「Redux vs Context API」といった単純なアーキテクチャ比較から、現在では**「不要なコンポーネント再レンダリングをいかに完全に防止し、CPUとメモリ演算を最適化するか」**というパフォーマンス主도の課題へと移行しました。特にReact 19およびReact Compiler導入環境においても、大規模アプリケーションの状態複雑性が増すにつれ、各ライブラリが採用するレンダリング最適化メカニズムを正しく理解することが不可欠です。
本記事では、2026年現在最も普及している3大リアクティブ状態管理パラダイムであるZustand(モジュール型セレクターストア)、Jotai(アトミックストア)、Signals(ファイングレイン・リアクティビティ)の動作原理と実戦的な最適化手法を詳しく比較解説します。
要約
- Zustand: 中央集権型ストアモデルでありながらContext Providerを必要とせず、
useShallowやセレクター関数を通じて必要な状態スライスのみをサブスクライブすることで、再レンダ링を完全に制御できます。- Jotai: Recoilのアトミックパラダイムを軽量化したモデルで、状態を独立した原子(Atom)単位に分割し、依存関係をDAG(有向非巡回グラフ)で管理します。トップダウン型のレンダリングボトルネックを回避します。
- Signals: ReactのVirtual DOMレンダリングサイクル自体をバイパスするFine-Grained Reactivityを提供します。コンポーネント関数を再実行せず、変更されたDOMノードを直接更新することで $O(1)$ の更新コストを実現します。
- 選択基準: 全域的なアプリ状態やバックエンドデータキャッシュの管理にはZustand、複雑な派生状態やフォーム/キャンバス単位の独立状態にはJotai、毎秒60fps以上のリアルタイムデータストリーミング(株価、IoTテレメトリ)にはSignalsが最適です。
1. 3大状態管理パラダイムのメカニズム比較
標準の useState および useContext は、親コンポーネントの状態変更時に子ツリーを再帰的に再レンダリングするトップダウン方式を採用しています。一方、モダンな状態ライブラリはツリー外部のExternal Storeで状態を保持し、変更通知を対象コンポーネントにのみ配信します。
[ Context API (Top-Down) ]
Provider State Change ➔ Re-render Provider ➔ Re-render ALL Children
[ Zustand (Selector Store) ]
External Store Change ➔ Run Selectors ➔ Compare (Object.is / Shallow) ➔ Re-render Subscriber Only
[ Jotai (Atomic Store) ]
Atom Value Change ➔ Trace Atom Dependency Graph ➔ Re-render Dependent Components Only
[ React Signals (Fine-Grained) ]
Signal Value Change ➔ Bypass Component Function ➔ Update Target DOM Node Directly
2. Zustand: セレクターとTransient Updateによる再レンダリング防止
ZustandはシンプルなHook型APIを提供し、Reactコンポーネントツリーの外部に純粋なJavaScriptオブジェクトとしてストアを保持します。
2.1 セレクター最適化と useShallow
Zustandにおける代表的なアンチパターンはストア全体を直接取得することです。これにより、ストア内の無関係な状態変更でもコンポーネントが再レンダリングされます。
// ❌ Bad: ストア全体を取得するため、userやtheme変更時にも再レンダリングが発生
const { items, addItem } = useCartStore();
// ⭕ Good: スライス取得時に useShallow を適用して参照比較を最適化
import { useShallow } from 'zustand/react/shallow';
const { items, totalAmount } = useCartStore(
useShallow((state) => ({
items: state.items,
totalAmount: state.totalAmount,
}))
);
3. Jotai: アトミック状態モデルと派生Atom
Jotaiは基本単位である atom を宣言し、それを組み合わせることで派生状態(Derived State)を構築します。
import { atom, useAtomValue } from 'jotai';
export const priceAtom = atom(100);
export const quantityAtom = atom(2);
// Read-Only Derived Atom: 依存関係が変更された場合のみ再計算
export const totalPriceAtom = atom((get) => get(priceAtom) * get(quantityAtom));
export function PriceSummary() {
const totalPrice = useAtomValue(totalPriceAtom);
return <div className="text-xl font-bold">Total: ${totalPrice}</div>;
}
4. React Signals: Virtual DOMの完全バイパス
Signals(@preact/signals-react 等)は状態を値ではなくObservableなラッパーとして扱います。
import { signal, computed } from '@preact/signals-react';
const count = signal(0);
const doubleCount = computed(() => count.value * 2);
export function CounterApp() {
console.log('CounterApp Component Rendered'); // マウント時の1回のみ実行
return (
<div className="p-4 space-y-2">
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
5. ベンチマーク比較まとめ
| 項目 | Zustand (v5.0) | Jotai (v2.10) | Signals (v2.0) |
|---|---|---|---|
| パラダイム | セレクターストア | アトミックDAG | ファイングレイン |
| バンドルサイズ | ~1.2 kB | ~3.4 kB | ~1.6 kB |
| 1,000件更新速度 | 12.4 ms | 14.1 ms | 1.8 ms (DOM直書き) |
| コンポーネント再描画 | セレクター比較依存 | Atom依存関係依存 | 完全バイパス可能 |