T-CREATOR

あなたの Jotai アプリ、遅くない?React DevTools と debugLabel でボトルネックを特定し、最適化する手順

あなたの Jotai アプリ、遅くない?React DevTools と debugLabel でボトルネックを特定し、最適化する手順

React アプリケーションの状態管理ライブラリとして注目を集める Jotai ですが、適切に使わないとパフォーマンスの問題が発生することがあります。「なんとなく重い気がする」「ユーザーからレスポンスが遅いと言われた」といった経験はありませんか?

本記事では、React DevTools と debugLabel を駆使して、Jotai アプリケーションのパフォーマンスボトルネックを科学的に特定し、効果的に最適化する具体的な手順をご紹介します。実際のエラーコードやパフォーマンス計測結果も交えながら、あなたのアプリケーションを高速化するための実践的なテクニックをお伝えします。

Jotai アプリが遅くなる典型的なサイン

ユーザーが感じるパフォーマンス劣化

Jotai アプリケーションのパフォーマンス問題は、ユーザーエクスペリエンスに直接的な影響を与えます。以下のような症状が現れた場合は、要注意です。

1. 入力レスポンスの遅延

typescript// 問題のあるパターン例
const userInputAtom = atom('');
const expensiveComputationAtom = atom((get) => {
  const input = get(userInputAtom);
  // 重い計算処理が毎回実行される
  return heavyComputation(input);
});

function InputComponent() {
  const [input, setInput] = useAtom(userInputAtom);
  const result = useAtomValue(expensiveComputationAtom);

  return (
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)} // 文字入力の度に重い計算が走る
      placeholder='入力が重い...'
    />
  );
}

ユーザーが感じる症状:

  • キーボード入力に対する反応が 200ms 以上遅れる
  • 入力中に画面がカクつく
  • IME(日本語入力)での変換候補表示が遅い

2. スクロール時のパフォーマンス劣化

typescript// 典型的な問題パターン
const itemsAtom = atom([...]); // 大量のデータ
const filteredItemsAtom = atom((get) => {
  const items = get(itemsAtom);
  // フィルタリング処理が毎回実行
  return items.filter(item => expensiveFilter(item));
});

function ItemList() {
  const items = useAtomValue(filteredItemsAtom);

  return (
    <div style={{ height: '400px', overflow: 'auto' }}>
      {items.map(item => (
        <ExpensiveItemComponent key={item.id} item={item} />
      ))}
    </div>
  );
}

ユーザーが感じる症状:

  • スクロール時の FPS が 30 以下に低下
  • スクロール中の要素描画が追いつかない
  • 慣性スクロールが途中で止まる

3. 画面遷移の遅延

typescript// 問題となるコード例
const globalStateAtom = atom({
  user: {
    /* 大量のユーザー情報 */
  },
  products: {
    /* 大量の商品データ */
  },
  cart: {
    /* カート情報 */
  },
  // ... その他多数のプロパティ
});

// 全ての画面で巨大な状態を購読
function AnyPageComponent() {
  const [globalState] = useAtom(globalStateAtom); // 過剰な依存

  // 実際は user.name しか使わない
  return <h1>Hello, {globalState.user.name}</h1>;
}

ユーザーが感じる症状:

  • ページ読み込み時間が 2 秒以上
  • 画面遷移時の白い画面表示時間が長い
  • ブラウザタブが「応答なし」状態になる

開発者が見落としがちな警告サイン

1. React DevTools での警告表示

React DevTools を開いたときに以下のような警告が表示されていませんか?

sql⚠️ Warning: Cannot update a component while rendering a different component
⚠️ Warning: Maximum update depth exceeded
⚠️ Warning: Each child in a list should have a unique "key" prop

これらの警告は、Jotai の使い方に問題があることを示している可能性があります。

2. コンソールでのパフォーマンス警告

javascript// ブラウザのコンソールに表示される典型的な警告
console.warn('[Violation] 'click' handler took 234ms');
console.warn('[Violation] Forced reflow while executing JavaScript took 156ms');
console.error('Warning: setState was called during render');

3. atom の過度な更新頻度

typescript// 問題のあるパターン
const clockAtom = atom(Date.now());

// 毎秒更新(これは問題)
setInterval(() => {
  // atomの更新頻度が高すぎる
  store.set(clockAtom, Date.now());
}, 1000);

// さらに問題:毎ミリ秒更新
setInterval(() => {
  store.set(clockAtom, Date.now());
}, 1); // 1ms ごとに更新は過剰

4. メモリ使用量の異常な増加

typescript// メモリリークを引き起こすパターン
const createDynamicAtom = (id) => {
  const dynamicAtom = atom(`value-${id}`);
  // atom への参照が保持され続ける
  globalAtomRegistry.set(id, dynamicAtom);
  return dynamicAtom;
};

// 問題:atom が作られ続けてメモリリーク
let id = 0;
setInterval(() => {
  createDynamicAtom(id++); // メモリリークの原因
}, 1000);

パフォーマンス問題の早期発見チェックリスト

開発中に以下の項目をチェックすることで、パフォーマンス問題を早期に発見できます。

📊 定量的チェック項目

チェック項目良好な状態要注意問題あり
初回レンダリング時間< 100ms100-300ms> 300ms
再レンダリング回数(操作 1 回あたり)< 5 回5-15 回> 15 回
JavaScript ヒープサイズ< 50MB50-100MB> 100MB
FPS(スクロール時)> 5030-50< 30

🔍 定性的チェック項目

✅ atom 設計のチェック

  • 1 つの atom が持つデータサイズは適切か?(目安:1KB 以下)
  • atom 間の依存関係は複雑になりすぎていないか?
  • 不要な computed atom が作られていないか?

✅ コンポーネント設計のチェック

  • useAtom の使用箇所は最小限に抑えられているか?
  • useAtomValue と useSetAtom を適切に使い分けているか?
  • 巨大なオブジェクトを直接 atom で管理していないか?

✅ 開発環境のチェック

  • React DevTools で再レンダリングのハイライトを確認しているか?
  • debugLabel が適切に設定されているか?
  • パフォーマンス計測を定期的に行っているか?

🚨 緊急対応が必要な症状

以下の症状が現れた場合は、緊急でパフォーマンス改善が必要です:

typescript// 緊急対応が必要なエラー例
// 1. Maximum call stack size exceeded
function InfiniteRenderLoop() {
  const [count, setCount] = useAtom(countAtom);

  // 無限ループの原因:レンダリング中の状態更新
  setCount(count + 1); // ❌ 絶対にやってはいけない

  return <div>{count}</div>;
}

// 2. Cannot read property of undefined
const brokenAtom = atom((get) => {
  const data = get(dataAtom);
  // data が undefined の場合にエラー
  return data.nonExistentProperty.value; // ❌ エラーの原因
});

// 3. Memory leak による OutOfMemory
const leakyAtom = atom(() => {
  // 毎回新しい巨大なオブジェクトを作成
  return new Array(1000000)
    .fill(0)
    .map((_, i) => ({ id: i, data: 'huge data' }));
});

これらの問題を発見した場合の対処法については、後続のセクションで詳しく解説します。

React DevTools で Jotai の内部を覗く

Profiler タブの基本的な使い方

React DevTools の Profiler は、Jotai アプリケーションのパフォーマンス分析において最も重要なツールです。正しい使い方をマスターすることで、問題の根本原因を迅速に特定できます。

Profiler の起動と初期設定

typescript// まず、測定用のラッパーコンポーネントを作成
import { Profiler, ProfilerProps } from 'react';

interface JotaiProfilerProps {
  children: React.ReactNode;
  id: string;
}

function JotaiProfiler({
  children,
  id,
}: JotaiProfilerProps) {
  const onRenderCallback: ProfilerProps['onRender'] = (
    profileId,
    phase,
    actualDuration,
    baseDuration,
    startTime,
    commitTime
  ) => {
    // パフォーマンスデータをログ出力
    console.log(`[Jotai Profiler] ${profileId}`, {
      phase, // 'mount' または 'update'
      actualDuration, // 実際のレンダリング時間
      baseDuration, // メモ化なしでのレンダリング時間
      startTime, // レンダリング開始時刻
      commitTime, // React がコミットした時刻
    });

    // 閾値を超えた場合に警告
    if (actualDuration > 100) {
      console.warn(
        `🐌 Slow render detected in ${profileId}: ${actualDuration}ms`
      );
    }
  };

  return (
    <Profiler id={id} onRender={onRenderCallback}>
      {children}
    </Profiler>
  );
}

// 使用例
function App() {
  return (
    <JotaiProfiler id='main-app'>
      <MainComponent />
    </JotaiProfiler>
  );
}

React DevTools での実際の操作手順

Step 1: 記録の開始

  1. ブラウザで F12 を押して DevTools を開く
  2. React タブをクリック
  3. Profiler サブタブを選択
  4. 🔴 Record ボタンをクリックして計測開始

Step 2: 問題のある操作を実行

typescript// 計測したい操作例
function SlowComponent() {
  const [items, setItems] = useAtom(itemsAtom);

  const handleAddItem = () => {
    // この操作のパフォーマンスを計測したい
    setItems((prev) => [...prev, createNewItem()]);
  };

  return <button onClick={handleAddItem}>Add Item</button>;
}

Step 3: 記録の停止と分析

  • ⏹️ Stop ボタンで記録終了
  • タイムライン上の任意の時点をクリックして詳細確認

計測結果の読み取り方

Profiler の結果画面では、以下の情報に注目してください:

java📊 Profiler Results Example:

Commit Duration: 234ms (⚠️ 警告:100ms以上)
├─ App (120ms)
   ├─ SlowComponent (89ms) ← 問題のコンポーネント
   │  ├─ ItemList (76ms)
   │  └─ ItemForm (13ms)
   └─ SideBar (31ms)

Why did this render?
・ State changed (itemsAtom)
・ Props changed
・ Parent re-rendered

Jotai 専用の計測テクニック

atom の更新頻度を可視化する

typescript// atom の更新を追跡するカスタムフック
function useAtomUpdateTracker<T>(
  anAtom: Atom<T>,
  atomName: string
) {
  const value = useAtomValue(anAtom);
  const updateCountRef = useRef(0);
  const lastUpdateRef = useRef(Date.now());

  useEffect(() => {
    updateCountRef.current++;
    const now = Date.now();
    const timeSinceLastUpdate = now - lastUpdateRef.current;

    console.log(`🔄 ${atomName} updated`, {
      updateCount: updateCountRef.current,
      timeSinceLastUpdate: `${timeSinceLastUpdate}ms`,
      newValue: value,
    });

    // 高頻度更新の警告
    if (timeSinceLastUpdate < 100) {
      console.warn(
        `⚡ High frequency update detected in ${atomName}`
      );
    }

    lastUpdateRef.current = now;
  }, [value, atomName]);

  return value;
}

// 使用例
function MonitoredComponent() {
  // 通常の useAtomValue の代わりに追跡版を使用
  const items = useAtomUpdateTracker(
    itemsAtom,
    'itemsAtom'
  );
  const user = useAtomUpdateTracker(userAtom, 'userAtom');

  return <div>{/* コンポーネントの内容 */}</div>;
}

atom の依存関係を可視化する

typescript// atom の依存関係を記録するデバッグユーティリティ
const atomDependencyTracker = new Map();

function trackAtomDependencies<T>(
  atom: Atom<T>,
  atomName: string
) {
  const originalRead = atom.read;

  atom.read = (get, options) => {
    const dependencies = [];

    // get 関数をプロキシして依存関係を記録
    const trackedGet = (dependentAtom: any) => {
      if (dependentAtom.debugLabel) {
        dependencies.push(dependentAtom.debugLabel);
      }
      return get(dependentAtom);
    };

    const result = originalRead(trackedGet, options);

    atomDependencyTracker.set(atomName, dependencies);
    console.log(
      `📊 ${atomName} dependencies:`,
      dependencies
    );

    return result;
  };

  return atom;
}

// 使用例
const userAtom = trackAtomDependencies(
  atom({ name: 'John', age: 30 }),
  'userAtom'
);

const userNameAtom = trackAtomDependencies(
  atom((get) => get(userAtom).name),
  'userNameAtom'
);

再レンダリングパターンの読み取り方

正常な再レンダリングパターン

typescript// 期待される再レンダリングフロー
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);

function NormalRenderPattern() {
  const [count, setCount] = useAtom(countAtom);
  const doubleCount = useAtomValue(doubleCountAtom);

  // このボタンクリックで期待される再レンダリング:
  // 1. countAtom 更新
  // 2. NormalRenderPattern 再レンダリング(count 変更)
  // 3. doubleCountAtom 再計算
  // 4. DoubleDisplay 再レンダリング(doubleCount 変更)
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>
        Count: {count}
      </button>
      <DoubleDisplay />
    </div>
  );
}

function DoubleDisplay() {
  const doubleCount = useAtomValue(doubleCountAtom);
  return <p>Double: {doubleCount}</p>;
}

問題のある再レンダリングパターン

typescript// ❌ 問題パターン1: 無限ループ
function InfiniteRenderLoop() {
  const [count, setCount] = useAtom(countAtom);

  // レンダリング中の状態更新は無限ループの原因
  useEffect(() => {
    setCount(count + 1); // ❌ count に依存している
  }, [count]); // ❌ 無限ループ

  return <div>{count}</div>;
}

// ❌ 問題パターン2: 不要な再レンダリング
const bigObjectAtom = atom({
  user: { name: 'John', settings: { theme: 'dark' } },
  products: [
    /* 大量のデータ */
  ],
  cart: [
    /* カートデータ */
  ],
});

function OnlyNeedsUserName() {
  const [bigObject] = useAtom(bigObjectAtom);

  // user.name しか使わないのに全体を購読
  // products や cart が変更されても再レンダリングされる
  return <h1>{bigObject.user.name}</h1>; // ❌ 過剰な依存
}

// ✅ 正しい修正版
const userNameAtom = atom(
  (get) => get(bigObjectAtom).user.name
);

function OptimizedUserName() {
  const userName = useAtomValue(userNameAtom);
  return <h1>{userName}</h1>; // ✅ 必要最小限の依存
}

DevTools で確認すべき警告サイン

React DevTools の Profiler で以下のパターンが見つかった場合は要注意です:

yaml🚨 警告サイン1: 短時間での大量再レンダリング
Timeline: 0ms ────100ms────200ms────300ms
                             
Renders:                        (1秒間に10回以上の再レンダリング)

🚨 警告サイン2: カスケード再レンダリング
Component Tree:
App (rendered)
├─ Header (rendered)  不要?
├─ Content (rendered)
  ├─ List (rendered)
  └─ Details (rendered)  不要?
└─ Footer (rendered)  不要?

🚨 警告サイン3: 長時間のレンダリング
Single Render Duration: 500ms
├─ Heavy computation: 450ms  ボトルネック
└─ DOM updates: 50ms

次のセクションでは、debugLabel を使ってこれらの問題をさらに詳細に分析する方法を解説します。

debugLabel で問題のある atom を特定する

debugLabel の設定方法と命名規則

debugLabel は、Jotai の atom にデバッグ用の名前を付ける機能です。適切に設定することで、パフォーマンス問題の原因となる atom を迅速に特定できます。

基本的な debugLabel の設定

typescript// 基本的な設定方法
const userAtom = atom({ name: 'John', age: 30 });
userAtom.debugLabel = 'userAtom';

const userNameAtom = atom((get) => get(userAtom).name);
userNameAtom.debugLabel = 'userNameAtom';

// より詳細な情報を含む命名
const fetchUserAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  return await fetchUser(userId);
});
fetchUserAtom.debugLabel = 'fetchUserAtom:async';

推奨される命名規則

効率的なデバッグのために、以下の命名規則を推奨します:

typescript// 1. 機能ベースの命名
const todoListAtom = atom([]);
todoListAtom.debugLabel = 'todos:list';

const completedTodosAtom = atom((get) =>
  get(todoListAtom).filter((todo) => todo.completed)
);
completedTodosAtom.debugLabel = 'todos:completed';

// 2. レイヤー別の命名
const apiUserAtom = atom(null);
apiUserAtom.debugLabel = 'api:user:data';

const uiUserNameAtom = atom(
  (get) => get(apiUserAtom)?.name || ''
);
uiUserNameAtom.debugLabel = 'ui:user:name';

// 3. 状態の種類別命名
const isLoadingAtom = atom(false);
isLoadingAtom.debugLabel = 'state:loading';

const errorMessageAtom = atom('');
errorMessageAtom.debugLabel = 'state:error';

// 4. 複雑な計算の場合
const expensiveComputationAtom = atom((get) => {
  const data = get(dataAtom);
  return heavyCalculation(data);
});
expensiveComputationAtom.debugLabel = 'compute:heavy:main';

開発環境でのデバッグ設定

typescript// 開発環境専用のデバッグ拡張
if (process.env.NODE_ENV === 'development') {
  // 全ての atom 作成を監視
  const originalAtom = atom;

  window.atom = function debugAtom(...args) {
    const createdAtom = originalAtom(...args);

    // スタックトレースから作成場所を特定
    const stack = new Error().stack;
    const caller = stack.split('\n')[2];
    const location = caller.match(/\((.*):(\d+):(\d+)\)/);

    if (location) {
      const [, file, line] = location;
      const fileName = file.split('/').pop();
      createdAtom.debugLabel = `${fileName}:${line}`;
    }

    console.log('🏷️ Atom created:', createdAtom.debugLabel);
    return createdAtom;
  };
}

開発環境でのログ活用術

atom 更新ログの詳細化

typescript// 詳細なログ出力機能
function createDebugStore() {
  const store = createStore();
  const originalSet = store.set;
  const originalGet = store.get;

  // set 操作の監視
  store.set = (atom, value) => {
    const atomLabel = atom.debugLabel || 'unnamed-atom';
    const startTime = performance.now();

    console.group(`🔄 Setting ${atomLabel}`);
    console.log('Previous value:', store.get(atom));
    console.log('New value:', value);

    const result = originalSet(atom, value);

    const endTime = performance.now();
    console.log(
      `⏱️ Set duration: ${(endTime - startTime).toFixed(
        2
      )}ms`
    );
    console.groupEnd();

    return result;
  };

  // get 操作の監視(重い計算の検出)
  store.get = (atom) => {
    const atomLabel = atom.debugLabel || 'unnamed-atom';
    const startTime = performance.now();

    const result = originalGet(atom);

    const endTime = performance.now();
    const duration = endTime - startTime;

    if (duration > 10) {
      // 10ms 以上の計算は警告
      console.warn(
        `🐌 Slow atom computation: ${atomLabel} (${duration.toFixed(
          2
        )}ms)`
      );
    }

    return result;
  };

  return store;
}

// 使用例
const debugStore = createDebugStore();

function App() {
  return (
    <Provider store={debugStore}>
      <MainApp />
    </Provider>
  );
}

パフォーマンス異常の自動検出

typescript// パフォーマンス監視システム
class JotaiPerformanceMonitor {
  private renderCounts = new Map<string, number>();
  private renderTimes = new Map<string, number[]>();
  private thresholds = {
    maxRenderTime: 100, // ms
    maxRenderCount: 10, // 1秒間の回数
    maxAverageTime: 50, // ms
  };

  trackRender(atomLabel: string, duration: number) {
    // レンダリング回数の記録
    const currentCount =
      this.renderCounts.get(atomLabel) || 0;
    this.renderCounts.set(atomLabel, currentCount + 1);

    // レンダリング時間の記録
    const times = this.renderTimes.get(atomLabel) || [];
    times.push(duration);

    // 直近10回のデータのみ保持
    if (times.length > 10) {
      times.shift();
    }
    this.renderTimes.set(atomLabel, times);

    // 異常検出
    this.detectAnomalies(atomLabel, duration, times);
  }

  private detectAnomalies(
    atomLabel: string,
    duration: number,
    times: number[]
  ) {
    // 1. 単発の遅いレンダリング
    if (duration > this.thresholds.maxRenderTime) {
      console.error(
        `🚨 Slow render detected: ${atomLabel} (${duration}ms)`
      );
      this.logDetailedInfo(atomLabel);
    }

    // 2. 平均時間の超過
    const average =
      times.reduce((a, b) => a + b, 0) / times.length;
    if (average > this.thresholds.maxAverageTime) {
      console.warn(
        `⚡ High average render time: ${atomLabel} (${average.toFixed(
          2
        )}ms)`
      );
    }

    // 3. 高頻度レンダリング
    setTimeout(() => {
      const count = this.renderCounts.get(atomLabel) || 0;
      if (count > this.thresholds.maxRenderCount) {
        console.warn(
          `🔥 High frequency renders: ${atomLabel} (${count} times/sec)`
        );
      }
      this.renderCounts.set(atomLabel, 0); // リセット
    }, 1000);
  }

  private logDetailedInfo(atomLabel: string) {
    console.group(
      `📊 Performance details for ${atomLabel}`
    );

    const times = this.renderTimes.get(atomLabel) || [];
    const average =
      times.reduce((a, b) => a + b, 0) / times.length;
    const max = Math.max(...times);
    const min = Math.min(...times);

    console.table({
      Average: `${average.toFixed(2)}ms`,
      Max: `${max.toFixed(2)}ms`,
      Min: `${min.toFixed(2)}ms`,
      'Recent renders': times.length,
    });

    console.groupEnd();
  }
}

// グローバルインスタンス
const performanceMonitor = new JotaiPerformanceMonitor();

// React コンポーネントでの使用
function MonitoredAtomComponent({ atomName, atom }) {
  const value = useAtomValue(atom);

  useEffect(() => {
    const startTime = performance.now();

    return () => {
      const endTime = performance.now();
      performanceMonitor.trackRender(
        atomName,
        endTime - startTime
      );
    };
  });

  return <div>{/* コンポーネントの内容 */}</div>;
}

複雑な依存関係の可視化手法

依存関係グラフの生成

typescript// atom 依存関係の可視化ツール
class AtomDependencyVisualizer {
  private dependencies = new Map<string, Set<string>>();
  private reverseDependencies = new Map<
    string,
    Set<string>
  >();

  addDependency(atomName: string, dependsOn: string) {
    // 順方向の依存関係
    if (!this.dependencies.has(atomName)) {
      this.dependencies.set(atomName, new Set());
    }
    this.dependencies.get(atomName)!.add(dependsOn);

    // 逆方向の依存関係
    if (!this.reverseDependencies.has(dependsOn)) {
      this.reverseDependencies.set(dependsOn, new Set());
    }
    this.reverseDependencies.get(dependsOn)!.add(atomName);
  }

  // 循環依存の検出
  detectCircularDependencies(): string[][] {
    const visited = new Set<string>();
    const recursionStack = new Set<string>();
    const cycles: string[][] = [];

    const dfs = (
      atomName: string,
      path: string[]
    ): void => {
      if (recursionStack.has(atomName)) {
        // 循環依存を発見
        const cycleStart = path.indexOf(atomName);
        cycles.push(
          path.slice(cycleStart).concat(atomName)
        );
        return;
      }

      if (visited.has(atomName)) return;

      visited.add(atomName);
      recursionStack.add(atomName);

      const deps =
        this.dependencies.get(atomName) || new Set();
      for (const dep of deps) {
        dfs(dep, [...path, atomName]);
      }

      recursionStack.delete(atomName);
    };

    for (const atomName of this.dependencies.keys()) {
      if (!visited.has(atomName)) {
        dfs(atomName, []);
      }
    }

    return cycles;
  }

  // Mermaid 形式のグラフ出力
  generateMermaidGraph(): string {
    let mermaid = 'graph TD\n';

    for (const [atomName, deps] of this.dependencies) {
      for (const dep of deps) {
        mermaid += `    ${dep} --> ${atomName}\n`;
      }
    }

    return mermaid;
  }

  // 影響範囲の分析
  analyzeImpact(atomName: string): {
    directDependents: string[];
    allDependents: string[];
    depth: number;
  } {
    const directDependents = Array.from(
      this.reverseDependencies.get(atomName) || new Set()
    );

    const allDependents = new Set<string>();
    let maxDepth = 0;

    const collectDependents = (
      name: string,
      depth: number
    ) => {
      maxDepth = Math.max(maxDepth, depth);
      const dependents =
        this.reverseDependencies.get(name) || new Set();

      for (const dependent of dependents) {
        if (!allDependents.has(dependent)) {
          allDependents.add(dependent);
          collectDependents(dependent, depth + 1);
        }
      }
    };

    collectDependents(atomName, 0);

    return {
      directDependents,
      allDependents: Array.from(allDependents),
      depth: maxDepth,
    };
  }

  // パフォーマンス影響度の計算
  calculatePerformanceImpact(atomName: string): {
    riskLevel: 'low' | 'medium' | 'high';
    reasons: string[];
  } {
    const impact = this.analyzeImpact(atomName);
    const reasons: string[] = [];
    let riskLevel: 'low' | 'medium' | 'high' = 'low';

    // 直接依存者数が多い
    if (impact.directDependents.length > 5) {
      reasons.push(
        `${impact.directDependents.length} direct dependents`
      );
      riskLevel = 'medium';
    }

    // 全体の影響範囲が広い
    if (impact.allDependents.length > 10) {
      reasons.push(
        `${impact.allDependents.length} total dependents`
      );
      riskLevel = 'high';
    }

    // 依存の深さが深い
    if (impact.depth > 3) {
      reasons.push(`dependency depth: ${impact.depth}`);
      riskLevel = 'high';
    }

    return { riskLevel, reasons };
  }
}

// 使用例
const visualizer = new AtomDependencyVisualizer();

// atom 作成時に依存関係を記録
function createTrackedAtom<T>(
  read: (get: Getter) => T,
  debugLabel: string,
  dependencies: string[] = []
) {
  const atomInstance = atom(read);
  atomInstance.debugLabel = debugLabel;

  // 依存関係を記録
  dependencies.forEach((dep) => {
    visualizer.addDependency(debugLabel, dep);
  });

  return atomInstance;
}

// 実際の使用例
const userAtom = createTrackedAtom(
  () => ({ name: 'John', age: 30 }),
  'userAtom'
);

const userNameAtom = createTrackedAtom(
  (get) => get(userAtom).name,
  'userNameAtom',
  ['userAtom']
);

const userDisplayAtom = createTrackedAtom(
  (get) => `User: ${get(userNameAtom)}`,
  'userDisplayAtom',
  ['userNameAtom']
);

// 循環依存のチェック
const cycles = visualizer.detectCircularDependencies();
if (cycles.length > 0) {
  console.error(
    '🔄 Circular dependencies detected:',
    cycles
  );
}

// 特定の atom の影響範囲分析
const impact = visualizer.analyzeImpact('userAtom');
console.log('📈 Impact analysis for userAtom:', impact);

// Mermaid グラフの出力
console.log(
  '📊 Dependency graph:',
  visualizer.generateMermaidGraph()
);

この可視化ツールを使用することで、以下のような問題を早期に発見できます:

  1. 循環依存の検出: atom 同士が相互に参照している問題
  2. 過剰な依存関係: 1 つの atom が多数の atom に影響を与える問題
  3. 深い依存チェーン: 依存関係が深くなりすぎている問題
  4. パフォーマンス影響度: atom の変更がシステム全体に与える影響の大きさ

次のセクションでは、これらの情報を元に実際のボトルネック特定の手順を解説します。

ボトルネック特定の実践的手順

Step-by-Step デバッグフロー

パフォーマンス問題を体系的に解決するため、以下の手順に従ってデバッグを進めます。

Step 1: 問題の定量化

まず、問題を数値で把握します。

typescript// パフォーマンス計測ツールの設定
class PerformanceBenchmark {
  private metrics = {
    renderTime: [],
    updateTime: [],
    memoryUsage: [],
  };

  startMeasurement(label: string) {
    const startTime = performance.now();
    const startMemory =
      (performance as any).memory?.usedJSHeapSize || 0;

    return {
      end: () => {
        const endTime = performance.now();
        const endMemory =
          (performance as any).memory?.usedJSHeapSize || 0;

        const duration = endTime - startTime;
        const memoryDelta = endMemory - startMemory;

        console.log(`📊 ${label} Performance:`, {
          duration: `${duration.toFixed(2)}ms`,
          memoryDelta: `${(
            memoryDelta /
            1024 /
            1024
          ).toFixed(2)}MB`,
        });

        return { duration, memoryDelta };
      },
    };
  }

  // 自動計測の設定
  setupAutomaticMeasurement() {
    // レンダリング時間の計測
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (
          entry.entryType === 'measure' &&
          entry.name.includes('React')
        ) {
          this.metrics.renderTime.push(entry.duration);

          if (entry.duration > 100) {
            console.warn(
              `🐌 Slow render: ${entry.name} (${entry.duration}ms)`
            );
          }
        }
      }
    });

    observer.observe({ entryTypes: ['measure'] });
  }
}

const benchmark = new PerformanceBenchmark();
benchmark.setupAutomaticMeasurement();

// 使用例
function SlowComponent() {
  const measurement = benchmark.startMeasurement(
    'SlowComponent render'
  );

  const [items] = useAtom(itemsAtom);
  const processedItems = useMemo(() => {
    // 重い処理
    return items.map((item) => expensiveProcessing(item));
  }, [items]);

  useEffect(() => {
    measurement.end();
  });

  return <div>{/* コンポーネントの内容 */}</div>;
}

Step 2: 問題箇所の絞り込み

React DevTools と debugLabel を使って問題箇所を特定します。

typescript// 問題のある atom を特定するヘルパー
function identifyProblematicAtoms() {
  const atomPerformanceData = new Map();

  // 各 atom の使用状況を監視
  const trackAtomUsage = (
    atomName: string,
    operation: 'read' | 'write',
    duration: number
  ) => {
    if (!atomPerformanceData.has(atomName)) {
      atomPerformanceData.set(atomName, {
        readCount: 0,
        writeCount: 0,
        totalReadTime: 0,
        totalWriteTime: 0,
        maxReadTime: 0,
        maxWriteTime: 0,
      });
    }

    const data = atomPerformanceData.get(atomName);

    if (operation === 'read') {
      data.readCount++;
      data.totalReadTime += duration;
      data.maxReadTime = Math.max(
        data.maxReadTime,
        duration
      );
    } else {
      data.writeCount++;
      data.totalWriteTime += duration;
      data.maxWriteTime = Math.max(
        data.maxWriteTime,
        duration
      );
    }
  };

  // 問題のある atom のレポート生成
  const generateReport = () => {
    const problematicAtoms = [];

    for (const [atomName, data] of atomPerformanceData) {
      const avgReadTime =
        data.totalReadTime / data.readCount;
      const avgWriteTime =
        data.totalWriteTime / data.writeCount;

      const issues = [];

      // 読み取りが遅い
      if (avgReadTime > 10) {
        issues.push(
          `Slow reads (avg: ${avgReadTime.toFixed(2)}ms)`
        );
      }

      // 書き込みが遅い
      if (avgWriteTime > 10) {
        issues.push(
          `Slow writes (avg: ${avgWriteTime.toFixed(2)}ms)`
        );
      }

      // 読み取り頻度が高い
      if (data.readCount > 100) {
        issues.push(
          `High read frequency (${data.readCount} times)`
        );
      }

      // 書き込み頻度が高い
      if (data.writeCount > 50) {
        issues.push(
          `High write frequency (${data.writeCount} times)`
        );
      }

      if (issues.length > 0) {
        problematicAtoms.push({
          atomName,
          issues,
          data,
        });
      }
    }

    // 深刻度で並び替え
    problematicAtoms.sort(
      (a, b) => b.issues.length - a.issues.length
    );

    console.group('🔍 Problematic Atoms Report');
    problematicAtoms.forEach(
      ({ atomName, issues, data }) => {
        console.group(`⚠️ ${atomName}`);
        console.log('Issues:', issues);
        console.table(data);
        console.groupEnd();
      }
    );
    console.groupEnd();

    return problematicAtoms;
  };

  return { trackAtomUsage, generateReport };
}

// デバッグセッションの開始
const { trackAtomUsage, generateReport } =
  identifyProblematicAtoms();

// 一定時間後にレポート生成
setTimeout(() => {
  const problematicAtoms = generateReport();

  if (problematicAtoms.length > 0) {
    console.log(
      '🎯 Focus on these atoms for optimization:',
      problematicAtoms.slice(0, 3).map((p) => p.atomName)
    );
  }
}, 10000); // 10秒後にレポート

Step 3: 根本原因の分析

特定された問題箇所について、根本原因を深掘りします。

typescript// 根本原因分析ツール
class RootCauseAnalyzer {
  analyzeAtom(atomName: string, atom: any) {
    const analysis = {
      atomType: this.getAtomType(atom),
      complexity: this.calculateComplexity(atom),
      dependencies: this.getDependencies(atom),
      potentialIssues: [],
      recommendations: [],
    };

    // 問題パターンの検出
    this.detectCommonIssues(analysis);

    return analysis;
  }

  private getAtomType(atom: any): string {
    if (atom.read && atom.write) return 'read-write';
    if (atom.read) return 'read-only';
    return 'primitive';
  }

  private calculateComplexity(atom: any): number {
    if (!atom.read) return 1;

    // read 関数のコード解析(簡易版)
    const readCode = atom.read.toString();

    let complexity = 1;

    // ループの検出
    complexity +=
      (readCode.match(/for\s*\(/g) || []).length * 3;
    complexity +=
      (readCode.match(/\.map\(/g) || []).length * 2;
    complexity +=
      (readCode.match(/\.filter\(/g) || []).length * 2;
    complexity +=
      (readCode.match(/\.reduce\(/g) || []).length * 4;

    // 条件分岐の検出
    complexity += (readCode.match(/if\s*\(/g) || []).length;
    complexity += (readCode.match(/\?\s*:/g) || []).length;

    // 非同期処理の検出
    complexity +=
      (readCode.match(/await/g) || []).length * 2;

    return complexity;
  }

  private getDependencies(atom: any): string[] {
    if (!atom.read) return [];

    const dependencies = [];
    const readCode = atom.read.toString();

    // get() 呼び出しの検出
    const getMatches =
      readCode.match(/get\s*\(\s*([^)]+)\s*\)/g) || [];

    getMatches.forEach((match) => {
      const atomRef = match.match(
        /get\s*\(\s*([^)]+)\s*\)/
      )[1];
      if (atomRef.includes('Atom')) {
        dependencies.push(atomRef);
      }
    });

    return dependencies;
  }

  private detectCommonIssues(analysis: any) {
    const { complexity, dependencies, atomType } = analysis;

    // 複雑度が高い
    if (complexity > 10) {
      analysis.potentialIssues.push({
        type: 'high-complexity',
        severity: 'high',
        description: `Complexity score: ${complexity}`,
      });

      analysis.recommendations.push(
        'Consider breaking down the atom into smaller, focused atoms'
      );
    }

    // 依存関係が多い
    if (dependencies.length > 5) {
      analysis.potentialIssues.push({
        type: 'too-many-dependencies',
        severity: 'medium',
        description: `${dependencies.length} dependencies detected`,
      });

      analysis.recommendations.push(
        'Reduce dependencies or use derived atoms for intermediate calculations'
      );
    }

    // 非同期 atom での問題
    if (
      atomType === 'read-only' &&
      analysis.complexity > 5
    ) {
      analysis.potentialIssues.push({
        type: 'complex-async-atom',
        severity: 'high',
        description: 'Complex logic in async atom',
      });

      analysis.recommendations.push(
        'Move complex logic outside the atom or use separate atoms for each async operation'
      );
    }
  }

  // 使用例とレポート生成
  generateDetailedReport(atoms: Record<string, any>) {
    console.group('🔬 Root Cause Analysis Report');

    for (const [name, atom] of Object.entries(atoms)) {
      const analysis = this.analyzeAtom(name, atom);

      if (analysis.potentialIssues.length > 0) {
        console.group(`⚠️ ${name}`);
        console.log('Type:', analysis.atomType);
        console.log('Complexity:', analysis.complexity);
        console.log('Dependencies:', analysis.dependencies);
        console.log('Issues:', analysis.potentialIssues);
        console.log(
          'Recommendations:',
          analysis.recommendations
        );
        console.groupEnd();
      }
    }

    console.groupEnd();
  }
}

// 使用例
const analyzer = new RootCauseAnalyzer();

// 分析対象の atom を集める
const atomsToAnalyze = {
  userAtom,
  userNameAtom,
  expensiveComputationAtom,
  // ... その他の atom
};

analyzer.generateDetailedReport(atomsToAnalyze);

よくあるパフォーマンス問題のパターン

パターン 1: 過剰な再計算

typescript// ❌ 問題のあるコード
const expensiveAtom = atom((get) => {
  const data = get(dataAtom);
  // 毎回重い計算が実行される
  return data.map((item) => heavyCalculation(item));
});

// ✅ 改善されたコード
const memoizedExpensiveAtom = atom((get) => {
  const data = get(dataAtom);

  // 前回の結果をキャッシュ
  const cache = useMemo(
    () => data.map((item) => heavyCalculation(item)),
    [data]
  );

  return cache;
});

// さらに良い解決策: 分割して管理
const dataItemsAtom = atom([]);
const processedItemAtom = (index: number) =>
  atom((get) => {
    const items = get(dataItemsAtom);
    return heavyCalculation(items[index]);
  });

パターン 2: 不要な依存関係

typescript// ❌ 問題のあるコード
const bigObjectAtom = atom({
  user: { name: 'John', email: 'john@example.com' },
  settings: { theme: 'dark', language: 'en' },
  data: [
    /* 大量のデータ */
  ],
});

function UserComponent() {
  const [bigObject] = useAtom(bigObjectAtom);
  // user.name しか使わないのに全体を購読
  return <h1>{bigObject.user.name}</h1>;
}

// ✅ 改善されたコード
const userAtom = atom((get) => get(bigObjectAtom).user);
const userNameAtom = atom((get) => get(userAtom).name);

function OptimizedUserComponent() {
  const userName = useAtomValue(userNameAtom);
  return <h1>{userName}</h1>;
}

パターン 3: 無限ループ

typescript// ❌ 無限ループを起こすコード
const counterAtom = atom(0);
const autoIncrementAtom = atom(
  (get) => get(counterAtom),
  (get, set, value) => {
    set(counterAtom, value);
    // ❌ 自分自身を更新して無限ループ
    setTimeout(
      () => set(autoIncrementAtom, value + 1),
      1000
    );
  }
);

// ✅ 正しい実装
const counterAtom = atom(0);
const startAutoIncrement = () => {
  setInterval(() => {
    store.set(counterAtom, (prev) => prev + 1);
  }, 1000);
};

原因特定のためのチェックポイント

パフォーマンス問題の診断チェックリスト

チェック項目確認方法良好要改善
初回レンダリング時間React DevTools Profiler< 100ms> 300ms
再レンダリング頻度コンソールログ< 5 回/操作> 15 回/操作
atom 依存関係数debugLabel ログ< 3 個> 7 個
計算複雑度上記ツールで測定< 5> 15
メモリ使用量DevTools Memory タブ< 50MB> 100MB

緊急度判定フローチャート

typescript// 緊急度判定ロジック
function assessPerformanceUrgency(metrics: {
  renderTime: number;
  rerenderCount: number;
  memoryUsage: number;
  userComplaintCount: number;
}) {
  let urgencyScore = 0;
  const issues = [];

  // レンダリング時間の評価
  if (metrics.renderTime > 500) {
    urgencyScore += 10;
    issues.push('Critical: Render time > 500ms');
  } else if (metrics.renderTime > 200) {
    urgencyScore += 5;
    issues.push('Warning: Render time > 200ms');
  }

  // 再レンダリング回数の評価
  if (metrics.rerenderCount > 20) {
    urgencyScore += 8;
    issues.push('Critical: Too many re-renders');
  } else if (metrics.rerenderCount > 10) {
    urgencyScore += 4;
    issues.push('Warning: High re-render count');
  }

  // メモリ使用量の評価
  if (metrics.memoryUsage > 200) {
    urgencyScore += 7;
    issues.push('Critical: High memory usage');
  } else if (metrics.memoryUsage > 100) {
    urgencyScore += 3;
    issues.push('Warning: Moderate memory usage');
  }

  // ユーザーからの苦情
  if (metrics.userComplaintCount > 0) {
    urgencyScore += metrics.userComplaintCount * 2;
    issues.push(
      `User complaints: ${metrics.userComplaintCount}`
    );
  }

  // 緊急度の判定
  let urgencyLevel: 'low' | 'medium' | 'high' | 'critical';
  if (urgencyScore >= 15) urgencyLevel = 'critical';
  else if (urgencyScore >= 10) urgencyLevel = 'high';
  else if (urgencyScore >= 5) urgencyLevel = 'medium';
  else urgencyLevel = 'low';

  return {
    urgencyLevel,
    urgencyScore,
    issues,
    recommendedAction: getRecommendedAction(urgencyLevel),
  };
}

function getRecommendedAction(
  urgencyLevel: string
): string {
  switch (urgencyLevel) {
    case 'critical':
      return '即座に作業を停止し、パフォーマンス改善に集中してください';
    case 'high':
      return '今日中にパフォーマンス改善を開始してください';
    case 'medium':
      return '今週中にパフォーマンス改善を計画してください';
    case 'low':
      return '継続的な監視を続けてください';
    default:
      return '状況を再評価してください';
  }
}

// 使用例
const currentMetrics = {
  renderTime: 150,
  rerenderCount: 8,
  memoryUsage: 75,
  userComplaintCount: 1,
};

const assessment = assessPerformanceUrgency(currentMetrics);
console.log('🚨 Performance Assessment:', assessment);

次のセクションでは、特定された問題に対する具体的な解決策を詳しく解説します。

発見した問題の解決策と最適化手法

atom 分割による最適化

大きな atom を小さく分割することで、不要な再レンダリングを大幅に削減できます。

typescript// ❌ Before: 巨大な atom で全てを管理
const appStateAtom = atom({
  user: {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    preferences: {
      theme: 'dark',
      language: 'en',
      notifications: true,
    },
  },
  todos: [
    { id: 1, text: 'Learn Jotai', completed: false },
    {
      id: 2,
      text: 'Optimize performance',
      completed: true,
    },
  ],
  ui: {
    isLoading: false,
    activeTab: 'todos',
    sidebarOpen: true,
  },
});

// ✅ After: 目的別に atom を分割
const userAtom = atom({
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
});

const userPreferencesAtom = atom({
  theme: 'dark',
  language: 'en',
  notifications: true,
});

const todosAtom = atom([
  { id: 1, text: 'Learn Jotai', completed: false },
  { id: 2, text: 'Optimize performance', completed: true },
]);

const uiStateAtom = atom({
  isLoading: false,
  activeTab: 'todos',
  sidebarOpen: true,
});

// さらに細分化した atom
const userNameAtom = atom((get) => get(userAtom).name);
const userThemeAtom = atom(
  (get) => get(userPreferencesAtom).theme
);
const activeTodosAtom = atom((get) =>
  get(todosAtom).filter((todo) => !todo.completed)
);

不要な依存関係の排除

依存関係を最小限に抑えることで、パフォーマンスが大幅に向上します。

typescript// ❌ 問題: 不要な依存関係が多い
const heavyComputationAtom = atom((get) => {
  const user = get(userAtom); // 必要
  const todos = get(todosAtom); // 必要
  const ui = get(uiStateAtom); // 不要
  const preferences = get(userPreferencesAtom); // 不要

  return todos.map((todo) => ({
    ...todo,
    assignedTo: user.name,
  }));
});

// ✅ 解決: 必要な依存関係のみ
const optimizedComputationAtom = atom((get) => {
  const userName = get(userNameAtom); // より具体的な依存
  const todos = get(todosAtom);

  return todos.map((todo) => ({
    ...todo,
    assignedTo: userName,
  }));
});

// さらに最適化: 中間 atom を活用
const todoAssignmentMapAtom = atom((get) => {
  const userName = get(userNameAtom);
  return (todo) => ({ ...todo, assignedTo: userName });
});

const assignedTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const assignmentMapper = get(todoAssignmentMapAtom);
  return todos.map(assignmentMapper);
});

メモ化とキャッシング戦略

selectAtom によるメモ化

typescriptimport { selectAtom } from 'jotai/utils';

// 重い計算結果をメモ化
const expensiveDataAtom = atom([
  /* 大量のデータ */
]);

// selectAtom で部分的な選択とメモ化
const expensiveFilteredDataAtom = selectAtom(
  expensiveDataAtom,
  (data) => data.filter((item) => expensiveFilter(item)),
  (a, b) =>
    a.length === b.length &&
    a.every((item, i) => item.id === b[i].id)
);

// より細かい粒度でのメモ化
const itemByIdAtom = (id: number) =>
  selectAtom(
    expensiveDataAtom,
    (data) => data.find((item) => item.id === id),
    (a, b) => a?.id === b?.id
  );

カスタムキャッシング戦略

typescript// LRU キャッシュを使った atom
class LRUCache<K, V> {
  private cache = new Map<K, V>();
  private maxSize: number;

  constructor(maxSize: number = 100) {
    this.maxSize = maxSize;
  }

  get(key: K): V | undefined {
    const value = this.cache.get(key);
    if (value !== undefined) {
      // LRU: アクセスされたアイテムを最新に移動
      this.cache.delete(key);
      this.cache.set(key, value);
    }
    return value;
  }

  set(key: K, value: V): void {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.maxSize) {
      // 最も古いアイテムを削除
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }
}

const computationCache = new LRUCache<string, any>(50);

const cachedComputationAtom = atom((get) => {
  const input = get(inputAtom);
  const cacheKey = JSON.stringify(input);

  let result = computationCache.get(cacheKey);
  if (result === undefined) {
    result = heavyComputation(input);
    computationCache.set(cacheKey, result);
    console.log('🔄 Cache miss - computed new result');
  } else {
    console.log('⚡ Cache hit - returned cached result');
  }

  return result;
});

継続的なパフォーマンス監視

自動化されたパフォーマンステスト

typescript// パフォーマンステスト用のヘルパー
class JotaiPerformanceTest {
  private testResults = new Map<
    string,
    {
      renderTime: number[];
      updateTime: number[];
      memoryUsage: number[];
    }
  >();

  async runPerformanceTest(
    testName: string,
    testFunction: () => Promise<void>,
    iterations: number = 10
  ) {
    const results = {
      renderTime: [],
      updateTime: [],
      memoryUsage: [],
    };

    for (let i = 0; i < iterations; i++) {
      const startMemory =
        (performance as any).memory?.usedJSHeapSize || 0;
      const startTime = performance.now();

      await testFunction();

      const endTime = performance.now();
      const endMemory =
        (performance as any).memory?.usedJSHeapSize || 0;

      results.renderTime.push(endTime - startTime);
      results.memoryUsage.push(
        (endMemory - startMemory) / 1024 / 1024
      );
    }

    this.testResults.set(testName, results);
    this.generateReport(testName, results);
  }

  private generateReport(testName: string, results: any) {
    const avgRenderTime =
      results.renderTime.reduce((a, b) => a + b, 0) /
      results.renderTime.length;
    const maxRenderTime = Math.max(...results.renderTime);
    const avgMemoryUsage =
      results.memoryUsage.reduce((a, b) => a + b, 0) /
      results.memoryUsage.length;

    console.group(`📊 Performance Test: ${testName}`);
    console.table({
      'Average Render Time': `${avgRenderTime.toFixed(
        2
      )}ms`,
      'Max Render Time': `${maxRenderTime.toFixed(2)}ms`,
      'Average Memory Usage': `${avgMemoryUsage.toFixed(
        2
      )}MB`,
    });

    // 性能基準値との比較
    if (avgRenderTime > 100) {
      console.warn(
        '⚠️ Average render time exceeds 100ms threshold'
      );
    }
    if (maxRenderTime > 500) {
      console.error(
        '🚨 Maximum render time exceeds 500ms threshold'
      );
    }
    if (avgMemoryUsage > 10) {
      console.warn(
        '⚠️ Average memory usage exceeds 10MB threshold'
      );
    }

    console.groupEnd();
  }

  // ベンチマーク比較
  compareWithBaseline(
    testName: string,
    baselineResults: any
  ) {
    const currentResults = this.testResults.get(testName);
    if (!currentResults) return;

    const currentAvg =
      currentResults.renderTime.reduce((a, b) => a + b, 0) /
      currentResults.renderTime.length;
    const baselineAvg =
      baselineResults.renderTime.reduce(
        (a, b) => a + b,
        0
      ) / baselineResults.renderTime.length;

    const improvement =
      ((baselineAvg - currentAvg) / baselineAvg) * 100;

    console.log(
      `📈 Performance comparison for ${testName}:`
    );
    console.log(`Baseline: ${baselineAvg.toFixed(2)}ms`);
    console.log(`Current: ${currentAvg.toFixed(2)}ms`);
    console.log(`Improvement: ${improvement.toFixed(2)}%`);
  }
}

// 使用例
const performanceTest = new JotaiPerformanceTest();

describe('Jotai Performance Tests', () => {
  test('Todo list rendering performance', async () => {
    await performanceTest.runPerformanceTest(
      'TodoList',
      async () => {
        // テスト対象の操作
        render(<TodoListComponent />);
        await screen.findByText('Todo List');
      }
    );
  });

  test('Large state update performance', async () => {
    await performanceTest.runPerformanceTest(
      'LargeStateUpdate',
      async () => {
        const { result } = renderHook(() =>
          useSetAtom(largeTodosAtom)
        );
        act(() => {
          result.current((prev) => [
            ...prev,
            ...generateLargeTodoList(1000),
          ]);
        });
      }
    );
  });
});

CI/CD での性能回帰検知

typescript// package.json scripts
{
  "scripts": {
    "perf:test": "jest --testMatch='**/*.perf.test.{js,ts}'",
    "perf:baseline": "yarn perf:test --outputFile=performance-baseline.json",
    "perf:compare": "node scripts/compare-performance.js"
  }
}

// scripts/compare-performance.js
const fs = require('fs');
const path = require('path');

function comparePerformanceResults() {
  const baselinePath = path.join(__dirname, '../performance-baseline.json');
  const currentPath = path.join(__dirname, '../performance-current.json');

  if (!fs.existsSync(baselinePath)) {
    console.log('📊 Creating new performance baseline');
    return;
  }

  const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
  const current = JSON.parse(fs.readFileSync(currentPath, 'utf8'));

  const regressions = [];
  const improvements = [];

  for (const [testName, currentResult] of Object.entries(current)) {
    const baselineResult = baseline[testName];
    if (!baselineResult) continue;

    const currentAvg = currentResult.renderTime.reduce((a, b) => a + b, 0) / currentResult.renderTime.length;
    const baselineAvg = baselineResult.renderTime.reduce((a, b) => a + b, 0) / baselineResult.renderTime.length;

    const change = ((currentAvg - baselineAvg) / baselineAvg) * 100;

    if (change > 20) { // 20%以上の性能劣化
      regressions.push({
        testName,
        change: change.toFixed(2),
        current: currentAvg.toFixed(2),
        baseline: baselineAvg.toFixed(2),
      });
    } else if (change < -10) { // 10%以上の性能向上
      improvements.push({
        testName,
        change: Math.abs(change).toFixed(2),
        current: currentAvg.toFixed(2),
        baseline: baselineAvg.toFixed(2),
      });
    }
  }

  if (regressions.length > 0) {
    console.error('🚨 Performance regressions detected:');
    console.table(regressions);
    process.exit(1);
  }

  if (improvements.length > 0) {
    console.log('🚀 Performance improvements detected:');
    console.table(improvements);
  }

  console.log('✅ No significant performance regressions detected');
}

comparePerformanceResults();

本番環境での監視体制

typescript// 本番環境用の軽量監視システム
class ProductionPerformanceMonitor {
  private isEnabled = process.env.NODE_ENV === 'production';
  private sampleRate = 0.1; // 10%のトラフィックをサンプリング

  constructor() {
    if (this.isEnabled && Math.random() < this.sampleRate) {
      this.initializeMonitoring();
    }
  }

  private initializeMonitoring() {
    // Core Web Vitals の監視
    this.observeWebVitals();

    // Jotai 特有のメトリクス監視
    this.observeJotaiMetrics();
  }

  private observeWebVitals() {
    // First Contentful Paint
    new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.name === 'first-contentful-paint') {
          this.reportMetric('FCP', entry.startTime);
        }
      });
    }).observe({ entryTypes: ['paint'] });

    // Largest Contentful Paint
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.reportMetric('LCP', lastEntry.startTime);
    }).observe({
      entryTypes: ['largest-contentful-paint'],
    });
  }

  private observeJotaiMetrics() {
    // atom 更新頻度の監視
    let atomUpdateCount = 0;
    const startTime = Date.now();

    const originalConsoleLog = console.log;
    console.log = (...args) => {
      if (
        args[0]?.includes?.('🔄') &&
        args[0]?.includes?.('updated')
      ) {
        atomUpdateCount++;
      }
      originalConsoleLog.apply(console, args);
    };

    // 1分間隔でメトリクスを送信
    setInterval(() => {
      const duration = (Date.now() - startTime) / 1000 / 60; // minutes
      const updateRate = atomUpdateCount / duration;

      if (updateRate > 60) {
        // 1分間に60回以上の更新は異常
        this.reportMetric(
          'high_atom_update_rate',
          updateRate
        );
      }

      atomUpdateCount = 0;
    }, 60000);
  }

  private reportMetric(metricName: string, value: number) {
    // 分析サービスに送信(例:Google Analytics, DataDog など)
    if (typeof gtag !== 'undefined') {
      gtag('event', 'jotai_performance', {
        metric_name: metricName,
        metric_value: value,
        user_agent: navigator.userAgent,
        timestamp: Date.now(),
      });
    }

    // 開発環境では詳細ログ
    if (process.env.NODE_ENV === 'development') {
      console.log(
        `📊 Performance Metric: ${metricName} = ${value}`
      );
    }
  }
}

// アプリケーション起動時に監視開始
const performanceMonitor =
  new ProductionPerformanceMonitor();

まとめ

本記事では、Jotai アプリケーションのパフォーマンス問題を効率的に特定し、解決するための包括的な手法をご紹介しました。

重要なポイントの振り返り

🔍 問題の早期発見

  • ユーザーエクスペリエンスの劣化症状を見逃さない
  • React DevTools Profiler の活用
  • debugLabel による atom の可視化
  • 定量的な性能指標の設定

⚡ 効果的な最適化手法

  • atom の適切な分割による責務の明確化
  • 不要な依存関係の排除
  • selectAtom とメモ化によるパフォーマンス向上
  • LRU キャッシュなどの高度なキャッシング戦略

📊 継続的な監視体制

  • 自動化されたパフォーマンステスト
  • CI/CD パイプラインでの性能回帰検知
  • 本番環境でのリアルタイム監視

実践で得られる成果

これらの手法を適用することで、以下のような具体的な改善が期待できます:

改善項目改善前改善後効果
初回レンダリング時間300-500ms50-100ms60-80%短縮
再レンダリング回数15-20 回/操作3-5 回/操作70-80%削減
メモリ使用量100-200MB30-60MB50-70%削減
ユーザー体感速度重い・遅い軽快・高速UX 大幅改善

次のステップ

パフォーマンス最適化は継続的なプロセスです。以下の点を意識して取り組んでください:

  1. 定期的な監視: 週次または月次でのパフォーマンス測定
  2. チーム内共有: 最適化のベストプラクティスをチーム全体で共有
  3. ユーザーフィードバック: 実際のユーザーからの性能に関するフィードバック収集
  4. 継続的改善: 新機能追加時の性能影響評価

あなたの Jotai アプリケーションが、ユーザーにとって快適で高速な体験を提供できることを願っています。パフォーマンス最適化は地道な作業ですが、その成果はユーザー満足度の向上として必ず現れるでしょう。

関連リンク