Procházet zdrojové kódy

update serena memories

Yuki Takei před 7 měsíci
rodič
revize
9ae0a77f2e

+ 375 - 0
.serena/memories/apps-app-modal-performance-optimization-v2.md

@@ -0,0 +1,375 @@
+# モーダル系コンポーネント パフォーマンス最適化ガイド Version2
+
+## 概要
+
+モーダル系コンポーネントは通常時は非表示状態であるにも関わらず、不適切な実装により常時リソースを消費している場合があります。本ドキュメントは、モーダルコンポーネントのパフォーマンス最適化の指針と具体的な手法を提供します。
+
+## 最適化の要否の評価基準
+
+### 高優先度(必須対応)
+
+以下の条件に**複数該当**する場合は、必ず最適化を実施してください:
+
+- ✅ **重い計算処理**: `useMemo`を使わずに毎レンダリングで配列操作(filter、map、reduce等)を実行
+- ✅ **外部データフェッチ**: SWRやカスタムフックによるAPI呼び出しが常時発生
+- ✅ **多数のフック**: useState、useEffect、useCallbackが5個以上存在
+- ✅ **インライン関数**: イベントハンドラーが関数宣言で定義されている
+- ✅ **複雑な条件分岐**: レンダリング時に重い条件判定を実行
+
+### 中優先度(推奨対応)
+
+以下の条件に該当する場合は、開発工数と効果を検討して対応を判断:
+
+- 🔶 **軽微な計算処理**: 単純な文字列操作や数値計算を毎レンダリングで実行
+- 🔶 **少数のフック**: useState、useEffect、useCallbackが2-4個存在
+- 🔶 **モーダル表示頻度**: ユーザーが頻繁に開閉するモーダル
+
+### 低優先度(対応不要)
+
+以下のような単純なモーダルは最適化不要:
+
+- ⚪ **静的コンテンツのみ**: テキストやボタンのみで計算処理なし
+- ⚪ **最小限のフック**: useState 1-2個程度のシンプルな実装
+- ⚪ **使用頻度が低い**: エラー表示など限定的な用途
+
+## 主要なパフォーマンス問題パターン
+
+### 1. コンテナ・プレゼンテーション分離の欠如
+
+**❌ 問題のあるパターン**
+```typescript
+const MyModal: FC = () => {
+  const { isOpen } = useModalStatus();
+  
+  // モーダルが閉じていても以下が常に実行される
+  const heavyData = useMemo(() => processHeavyData(data), [data]);
+  const { data: apiData } = useSWR('/api/data');
+  
+  return (
+    <Modal isOpen={isOpen}>
+      {/* 内容 */}
+    </Modal>
+  );
+};
+```
+
+**✅ 最適化されたパターン**
+```typescript
+const MyModalSubstance: FC = () => {
+  // 重い処理はモーダルが開いている時のみ実行される
+  const heavyData = useMemo(() => processHeavyData(data), [data]);
+  const { data: apiData } = useSWR('/api/data');
+  
+  return (
+    <>
+      <ModalHeader>...</ModalHeader>
+      <ModalBody>...</ModalBody>
+      <ModalFooter>...</ModalFooter>
+    </>
+  );
+};
+
+const MyModal: FC = () => {
+  const { isOpen } = useModalStatus();
+  
+  if (!isOpen) {
+    return <></>;  // 早期リターンで不要な処理を完全回避
+  }
+  
+  return (
+    <Modal isOpen={isOpen}>
+      <MyModalSubstance />
+    </Modal>
+  );
+};
+```
+
+### 2. 計算処理の最適化不足
+
+**❌ 問題のあるパターン**
+```typescript
+// 毎レンダリングで実行される重い計算
+const filteredItems = items.filter(item => item.status === 'active');
+const processedData = filteredItems.map(item => transformItem(item));
+const targetPath = clickedItem?.path || defaultPath;
+```
+
+**✅ 最適化されたパターン**
+```typescript
+const filteredItems = useMemo(() => 
+  items.filter(item => item.status === 'active'), 
+  [items]
+);
+
+const processedData = useMemo(() => 
+  filteredItems.map(item => transformItem(item)), 
+  [filteredItems]
+);
+
+const targetPath = useMemo(() => 
+  clickedItem?.path || defaultPath, 
+  [clickedItem, defaultPath]
+);
+```
+
+### 3. イベントハンドラーの最適化不足
+
+**❌ 問題のあるパターン**
+```typescript
+// 毎レンダリングで新しい関数が作成される
+function handleSubmit() {
+  // 処理
+}
+
+function handleCancel() {
+  closeModal();
+}
+```
+
+**✅ 最適化されたパターン**
+```typescript
+const handleSubmit = useCallback(() => {
+  // 処理
+}, [dependencies]);
+
+const handleCancel = useCallback(() => {
+  closeModal();
+}, [closeModal]);
+```
+
+### 4. レンダリング関数の最適化不足
+
+**❌ 問題のあるパターン**
+```typescript
+const renderItems = () => {
+  const displayItems = items.length > 0 ? items : defaultItems;
+  return displayItems.map(item => <ItemComponent key={item.id} item={item} />);
+};
+
+return (
+  <Modal>
+    {renderItems()}
+  </Modal>
+);
+```
+
+**✅ 最適化されたパターン**
+```typescript
+const displayItems = useMemo(() => 
+  items.length > 0 ? items : defaultItems, 
+  [items, defaultItems]
+);
+
+const renderedItems = useMemo(() =>
+  displayItems.map(item => <ItemComponent key={item.id} item={item} />),
+  [displayItems]
+);
+
+return (
+  <Modal>
+    {renderedItems}
+  </Modal>
+);
+```
+
+### **重要** メモ化の判断基準
+
+ただし、過度なメモ化は避ける
+
+- ✅ 重い計算処理: useMemoで保護
+- ✅ 複雑なオブジェクト構築: useMemoで保護
+- ✅ 外部依存のあるハンドラ: useCallbackで保護
+- ❌ 単純な条件分岐: メモ化不要
+- ❌ 軽量なsetter関数: useCallback不要
+
+
+
+## 最適化チェックリスト
+
+### 🔍 診断フェーズ
+
+- [ ] モーダルが閉じている状態でのフック実行数を確認
+- [ ] React DevTools Profilerでレンダリング回数を測定
+- [ ] Network タブで不要なAPI呼び出しがないか確認
+- [ ] コンソールで不要なログ出力がないか確認
+
+### 🚀 実装フェーズ
+
+#### 必須対応項目
+- [ ] **コンテナ・プレゼンテーション分離**: 早期リターンによる完全な処理回避を実装
+- [ ] **計算処理のメモ化**: 配列操作や文字列処理のうち、メモ化すべきと判断したものを `useMemo` でラップ
+- [ ] **イベントハンドラーのメモ化**: イベントハンドラーのうち、メモ化すべきと判断したものを `useCallback` でラップ
+- [ ] **外部依存関数の安定化**: 親から渡される関数の依存関係を明確化
+
+#### 推奨対応項目
+- [ ] **レンダリング関数の最適化**: 条件付きレンダリングを事前計算
+- [ ] **状態の適切な初期化**: useEffect での状態リセット処理を最適化
+- [ ] **型安全性の向上**: 分離後のコンポーネントでのnon-null assertionを活用
+
+### 🧪 検証フェーズ
+
+- [ ] **レンダリング回数の削減**: React DevTools Profilerで改善を確認
+- [ ] **メモリ使用量の削減**: 閉じた状態でのヒープ使用量を確認
+- [ ] **ネットワークリクエストの削減**: 不要なAPI呼び出しの停止を確認
+- [ ] **ユーザー体験の向上**: モーダル開閉速度の改善を確認
+
+## 実装テンプレート
+
+### 基本的なモーダル分離パターン
+
+```typescript
+// Substance: 実際のモーダル内容(重い処理を含む)
+const MyModalSubstance: FC = () => {
+  const { data, opts } = useMyModalStatus()!; // 非null確定
+  const { close } = useMyModalActions();
+  
+  // 重い処理はここで実行(モーダルが開いている時のみ)
+  const processedData = useMemo(() => 
+    heavyProcessing(data), 
+    [data]
+  );
+  
+  const handleAction = useCallback((item: Item) => {
+    // アクション処理
+    opts?.onAction?.(item);
+    close();
+  }, [opts?.onAction, close]);
+  
+  return (
+    <>
+      <ModalHeader toggle={close}>タイトル</ModalHeader>
+      <ModalBody>
+        {processedData.map(item => (
+          <ItemComponent key={item.id} item={item} onAction={handleAction} />
+        ))}
+      </ModalBody>
+      <ModalFooter>
+        <Button onClick={close}>キャンセル</Button>
+      </ModalFooter>
+    </>
+  );
+};
+
+// Container: モーダルの表示制御のみ
+export const MyModal: FC = () => {
+  const { isOpen } = useMyModalStatus() ?? {};
+  const { close } = useMyModalActions();
+  
+  if (!isOpen) {
+    return <></>;
+  }
+  
+  return (
+    <Modal isOpen={isOpen} toggle={close}>
+      <MyModalSubstance />
+    </Modal>
+  );
+};
+```
+
+### 複雑なデータ処理を含むモーダルパターン
+
+```typescript
+const ComplexModalSubstance: FC = () => {
+  const { items, filters, opts } = useComplexModalStatus()!;
+  const { close } = useComplexModalActions();
+  
+  // Step 1: 基本フィルタリング
+  const filteredItems = useMemo(() => 
+    items.filter(item => applyFilters(item, filters)),
+    [items, filters]
+  );
+  
+  // Step 2: 権限チェック
+  const accessibleItems = useMemo(() =>
+    filteredItems.filter(item => hasPermission(item)),
+    [filteredItems]
+  );
+  
+  // Step 3: ソート・グループ化
+  const organizedItems = useMemo(() => 
+    groupAndSort(accessibleItems),
+    [accessibleItems]
+  );
+  
+  // イベントハンドラー群
+  const handleSelect = useCallback((item: Item) => {
+    opts?.onSelect?.(item);
+    close();
+  }, [opts?.onSelect, close]);
+  
+  const handleBulkAction = useCallback((action: string) => {
+    const selectedItems = organizedItems.filter(item => item.selected);
+    opts?.onBulkAction?.(action, selectedItems);
+    close();
+  }, [organizedItems, opts?.onBulkAction, close]);
+  
+  return (
+    <>
+      <ModalHeader toggle={close}>複雑なモーダル</ModalHeader>
+      <ModalBody>
+        <ComplexContent 
+          items={organizedItems}
+          onSelect={handleSelect}
+          onBulkAction={handleBulkAction}
+        />
+      </ModalBody>
+      <ModalFooter>
+        <ActionButtons onBulkAction={handleBulkAction} />
+      </ModalFooter>
+    </>
+  );
+};
+```
+
+## 測定・監視のベストプラクティス
+
+### 開発時の測定
+```bash
+# React DevTools Profilerを使用
+# 1. モーダル閉じた状態でのベースライン測定
+# 2. モーダル開いた状態での差分測定
+# 3. 最適化前後での比較
+
+# Performance.measureUserTiming API活用例
+performance.mark('modal-render-start');
+// レンダリング処理
+performance.mark('modal-render-end');
+performance.measure('modal-render', 'modal-render-start', 'modal-render-end');
+```
+
+### 本番環境での監視
+- **Core Web Vitals**: FCP、LCP への影響を測定
+- **カスタムメトリクス**: モーダル開閉時間、レンダリング回数
+- **エラー監視**: メモリリークやパフォーマンス劣化の検知
+
+## まとめ
+
+モーダル系コンポーネントのパフォーマンス最適化は、**段階的なアプローチ**が重要です:
+
+### 🏃‍♂️ **Phase 1: 基本最適化(必須・全モーダル対象)**
+- **コンテナ・プレゼンテーション分離**: 早期リターンによる完全な処理回避
+- **適切なメモ化**: useMemo、useCallbackによる不要な再計算・再生成の回避
+    - ただし過度なメモ化をさけるためメモ化するかどうかの判断は適切に
+
+### 🚀 **Phase 2: 高度な最適化(条件次第で効果的)**
+- **Dynamic Import**: バンドル削減と初期読み込み速度向上
+- **プリロード戦略**: UXを損なわずにCode Splittingの効果を最大化
+
+### ⚠️ **実装時の重要な注意点**
+
+1. **技術的判断vs UX判断の区別**
+   - バンドルサイズ・使用頻度:技術的に測定可能
+   - 応答速度の期待値・ユーザー体験:**必ずステークホルダーに確認**
+
+2. **測定による検証**
+   - 最適化前後の具体的な数値比較を実施
+   - React DevTools Profiler、webpack-bundle-analyzerの活用
+
+3. **段階的実装**
+   - 高効果期待の重要モーダルから開始
+   - 効果を測定しながら対象を拡大
+
+特に**Dynamic Importの適用判断**では、技術的メトリクス(バンドルサイズ、使用頻度)は客観的に測定できますが、**ユーザー体験への影響は主観的**です。リファクタ前に必ず関係者との合意形成を行い、適切なフォールバック(プリロード、スケルトン表示等)を実装することを強く推奨します。
+
+最適化実施時は、必ず事前・事後の測定を行い、実際のパフォーマンス改善とユーザー体験の両面を数値で確認することが成功の鍵となります。

+ 0 - 215
.serena/memories/apps-app-modal-performance-optimization.md

@@ -1,215 +0,0 @@
-# Jotaiモーダル実装パフォーマンス最適化ガイド
-
-## 🎯 パフォーマンス最適化の基本原則
-
-### フック分離パターンによる最適化
-Jotaiでは`useAtom`の代わりに`useAtomValue`と`useSetAtom`を分離使用することで、不要なリレンダリングを防止できます。
-
-#### ❌ 非推奨パターン(リレンダリング発生)
-```typescript
-export const useModalState = () => {
-  const [state, setState] = useAtom(modalAtom); // 状態変更時に必ずリレンダリング
-  return { state, setState };
-};
-```
-
-#### ✅ 推奨パターン(最適化済み)
-```typescript
-// 読み取り専用 - 状態が変更された時のみリレンダリング
-export const useModalStatus = () => {
-  return useAtomValue(modalAtom);
-};
-
-// 書き込み専用 - リレンダリングなし、参照安定
-export const useModalActions = () => {
-  const setModal = useSetAtom(modalAtom);
-  
-  const open = useCallback((data) => {
-    setModal({ isOpened: true, ...data });
-  }, [setModal]);
-  
-  const close = useCallback(() => {
-    setModal({ isOpened: false });
-  }, [setModal]);
-  
-  return { open, close };
-};
-```
-
-## 📋 実装済みモーダル一覧(全17個)
-
-### 🎉 完全移行完了モーダル(パフォーマンス最適化済み)
-
-#### コアモーダル(2個)
-1. **PageCreateModal** - `~/states/ui/modal/page-create.ts`
-2. **PageDeleteModal** - `~/states/ui/modal/page-delete.ts`
-
-#### 第1バッチ(4個)
-3. **EmptyTrashModal** - `~/states/ui/modal/empty-trash.ts`
-4. **DeleteAttachmentModal** - `~/states/ui/modal/delete-attachment.ts`
-5. **DeleteBookmarkFolderModal** - `~/states/ui/modal/delete-bookmark-folder.ts`
-6. **UpdateUserGroupConfirmModal** - `~/states/ui/modal/update-user-group-confirm.ts`
-
-#### 第2バッチ(3個)
-7. **PageSelectModal** - `~/states/ui/modal/page-select.ts`
-8. **PagePresentationModal** - `~/states/ui/modal/page-presentation.ts`
-9. **PutBackPageModal** - `~/states/ui/modal/put-back-page.ts`
-
-#### 第3バッチ(3個)
-10. **GrantedGroupsInheritanceSelectModal** - `~/states/ui/modal/granted-groups-inheritance-select.ts`
-11. **DrawioModal** - `~/states/ui/modal/drawio.ts`
-12. **HandsontableModal** - `~/states/ui/modal/handsontable.ts`
-
-#### 第4バッチ(3個)
-13. **PrivateLegacyPagesMigrationModal** - `~/states/ui/modal/private-legacy-pages-migration.ts`
-14. **DescendantsPageListModal** - `~/states/ui/modal/descendants-page-list.ts`
-15. **ConflictDiffModal** - `~/states/ui/modal/conflict-diff.ts`
-
-#### 第5バッチ(4個)
-16. **PageBulkExportSelectModal** - `~/states/ui/modal/page-bulk-export-select.ts`
-17. **DrawioForEditorModal** - `~/states/ui/modal/drawio-for-editor.ts`
-18. **LinkEditModal** - `~/states/ui/modal/link-edit.ts`
-19. **TemplateModal** - `~/states/ui/modal/template.ts`
-
-## 🏗️ 統一された実装パターン
-
-### 基本テンプレート
-```typescript
-import { atom, useAtomValue, useSetAtom } from 'jotai';
-import { useCallback } from 'react';
-
-// 型定義
-type [Modal]State = {
-  isOpened: boolean;
-  // モーダル固有のプロパティ
-};
-
-// Atom定義
-const [modal]Atom = atom<[Modal]State>({
-  isOpened: false,
-  // デフォルト値
-});
-
-// 読み取り専用フック
-export const use[Modal]Status = () => {
-  return useAtomValue([modal]Atom);
-};
-
-// アクション専用フック
-export const use[Modal]Actions = () => {
-  const setModalState = useSetAtom([modal]Atom);
-
-  return {
-    open: useCallback((args) => {
-      setModalState({ isOpened: true, ...args });
-    }, [setModalState]),
-    close: useCallback(() => {
-      setModalState({ isOpened: false });
-    }, [setModalState]),
-  };
-};
-```
-
-### 複雑なモーダルの例(ConflictDiffModal)
-```typescript
-// 型定義
-type ResolveConflictHandler = (newMarkdown: string) => Promise<void> | void;
-
-type ConflictDiffModalState = {
-  isOpened: boolean;
-  requestRevisionBody?: string;
-  onResolve?: ResolveConflictHandler;
-};
-
-const conflictDiffModalAtom = atom<ConflictDiffModalState>({
-  isOpened: false,
-  requestRevisionBody: undefined,
-  onResolve: undefined,
-});
-
-export const useConflictDiffModalStatus = () => {
-  return useAtomValue(conflictDiffModalAtom);
-};
-
-export const useConflictDiffModalActions = () => {
-  const setModalState = useSetAtom(conflictDiffModalAtom);
-
-  return {
-    open: useCallback((requestRevisionBody: string, onResolve: ResolveConflictHandler) => {
-      setModalState({ isOpened: true, requestRevisionBody, onResolve });
-    }, [setModalState]),
-    close: useCallback(() => {
-      setModalState({ isOpened: false, requestRevisionBody: undefined, onResolve: undefined });
-    }, [setModalState]),
-  };
-};
-```
-
-## 🔧 使用方法
-
-### コンポーネントでの使用例
-```typescript
-// モーダルコンポーネント内
-const ModalComponent = () => {
-  const { isOpened, data } = useModalStatus(); // 状態のみ取得
-  const { close } = useModalActions(); // アクションのみ取得
-  
-  return (
-    <Modal isOpen={isOpened} toggle={close}>
-      {/* コンテンツ */}
-    </Modal>
-  );
-};
-
-// モーダル起動側
-const TriggerComponent = () => {
-  const { open } = useModalActions(); // アクションのみ取得
-  
-  return (
-    <button onClick={() => open(someData)}>
-      Open Modal
-    </button>
-  );
-};
-```
-
-## 📈 パフォーマンス効果
-
-### 最適化による効果
-1. **リレンダリング削減**: アクション専用フックはリレンダリングしない
-2. **参照安定性**: `useCallback`によりアクション関数が安定
-3. **メモリ効率**: 必要な状態のみ購読
-4. **型安全性**: TypeScriptによる完全な型チェック
-
-### 測定可能な改善
-- モーダル起動ボタンのリレンダリング: **ゼロ**
-- モーダル状態変更時の不要な再計算: **削減**
-- 開発者体験: **向上**(統一されたAPI)
-
-## 🎯 品質保証
-
-### 実装品質チェックリスト
-- ✅ `useAtomValue` / `useSetAtom` 分離パターン適用
-- ✅ `useCallback` によるアクション関数の安定化
-- ✅ TypeScript型定義の完全性
-- ✅ 全使用箇所の移行完了
-- ✅ 旧SWR実装の削除
-- ✅ `pnpm run lint:typecheck` 成功
-
-### 移行完了の確認方法
-```bash
-# 型チェック実行
-cd /workspace/growi/apps/app && pnpm run lint:typecheck
-
-# 旧実装が残っていないことを確認
-grep -r "useSWRStatic.*Modal" src/
-```
-
-## 🔄 更新履歴
-
-- **2025-09-05**: 第5バッチ完了、全17個のモーダル移行完了記録
-- **2025-09-05**: 第4バッチ実装パターン追加
-- **2025-09-05**: 第3バッチ複雑なモーダル例追加  
-- **2025-09-05**: 第2バッチパフォーマンス効果測定結果追加
-- **2025-09-05**: 第1バッチ実装完了、基本パターン確立
-- **2025-09-05**: 初版作成、パフォーマンス最適化パターン確立