あなたの 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);
パフォーマンス問題の早期発見チェックリスト
開発中に以下の項目をチェックすることで、パフォーマンス問題を早期に発見できます。
📊 定量的チェック項目
チェック項目 | 良好な状態 | 要注意 | 問題あり |
---|---|---|---|
初回レンダリング時間 | < 100ms | 100-300ms | > 300ms |
再レンダリング回数(操作 1 回あたり) | < 5 回 | 5-15 回 | > 15 回 |
JavaScript ヒープサイズ | < 50MB | 50-100MB | > 100MB |
FPS(スクロール時) | > 50 | 30-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: 記録の開始
- ブラウザで F12 を押して DevTools を開く
- React タブをクリック
- Profiler サブタブを選択
- 🔴 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()
);
この可視化ツールを使用することで、以下のような問題を早期に発見できます:
- 循環依存の検出: atom 同士が相互に参照している問題
- 過剰な依存関係: 1 つの atom が多数の atom に影響を与える問題
- 深い依存チェーン: 依存関係が深くなりすぎている問題
- パフォーマンス影響度: 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-500ms | 50-100ms | 60-80%短縮 |
再レンダリング回数 | 15-20 回/操作 | 3-5 回/操作 | 70-80%削減 |
メモリ使用量 | 100-200MB | 30-60MB | 50-70%削減 |
ユーザー体感速度 | 重い・遅い | 軽快・高速 | UX 大幅改善 |
次のステップ
パフォーマンス最適化は継続的なプロセスです。以下の点を意識して取り組んでください:
- 定期的な監視: 週次または月次でのパフォーマンス測定
- チーム内共有: 最適化のベストプラクティスをチーム全体で共有
- ユーザーフィードバック: 実際のユーザーからの性能に関するフィードバック収集
- 継続的改善: 新機能追加時の性能影響評価
あなたの Jotai アプリケーションが、ユーザーにとって快適で高速な体験を提供できることを願っています。パフォーマンス最適化は地道な作業ですが、その成果はユーザー満足度の向上として必ず現れるでしょう。
関連リンク
- article
あなたの Jotai アプリ、遅くない?React DevTools と debugLabel でボトルネックを特定し、最適化する手順
- article
Jotai アプリのパフォーマンスを極限まで高める selectAtom 活用術 - 不要な再レンダリングを撲滅せよ
- article
ローディングとエラー状態をスマートに分離。Jotai の loadable ユーティリティ徹底活用ガイド
- article
Jotai vs React Query(TanStack Query) - データフェッチング状態管理の使い分け
- article
Jotai ビジネスロジックを UI から分離する「アクション atom」という考え方
- article
モーダルやダイアログの「開閉状態」どこで持つ問題、Jotai ならこう解決する
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体
- review
瞬時に答えが出る脳に変身!『ゼロ秒思考』赤羽雄二が贈る思考力爆上げトレーニング
- review
関西弁のゾウに人生変えられた!『夢をかなえるゾウ 1』水野敬也が教えてくれた成功の本質