diff --git a/.jules/bolt.md b/.jules/bolt.md index f1a8c146..ca8782b5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -77,3 +77,7 @@ Optimized metric route processing to O(N) by creating a mapping of routes direct ## 2024-07-13 - [Optimize Export Dictionary FK lookups] **Learning:** Found O(N * C * E) performance bottleneck in ERD export dictionaries due to repeated array searching with `edges.some()` inside a nested loop over nodes and columns. **Action:** Replace repeated linear array scans for edges by precomputing O(1) Set lookups of foreign key column handles per node before looping. + +## 2024-05-19 - React Flow 데이터 변이와 성능 +**Learning:** useMemo 내부에서 노드 데이터를 변이하여 파생된 속성(예: isHighlighted/isDimmed)을 계산하면, 검색 키 입력 시마다 데이터 객체의 식별자가 변경되어 React Flow가 모든 노드를 다시 렌더링하게 만듭니다. 이는 모든 노드 컴포넌트에서 React.memo를 깨뜨리고 큰 그래프에서 검색 시 심각한 프레임 저하를 일으킵니다. +**Action:** 안정적인 node.data 객체를 키로 사용하는 WeakMap을 사용하여 장식된 데이터를 캐시합니다. 이렇게 하면 노드의 검색 일치 상태가 변경되지 않은 경우 렌더링 전반에 걸쳐 정확히 동일한 데이터 객체 참조를 유지하도록 보장하여 React.memo 성능을 유지하고 키 입력당 O(N) 객체 할당을 피할 수 있습니다. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d0dc258..0d192ec1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -116,6 +116,13 @@ function strengthLabel(strength: CardinalityStrength): string { return "보류"; } +// ⚡ Bolt: 객체 참조를 보존하여 불필요한 렌더링을 방지하기 위해 WeakMap을 사용해 검색 상태를 캐싱합니다. +// 안정적인 `node.data` 참조를 키로 사용하여 검색 강조 상태가 변경되지 않은 경우 +// 매 검색 입력마다 새로운 `data` 객체를 할당하는 것을 방지합니다. +// 이를 통해 React Flow가 수천 개의 노드를 불필요하게 다시 렌더링하는 것을 막습니다. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const searchDataCache = new WeakMap(); + export default function App() { const [activeView, setActiveView] = useState("dashboard"); const [me, setMe] = useState(null); @@ -201,13 +208,18 @@ export default function App() { if (!normalizedNodeSearch) return nodes; return nodes.map((node) => { const isHighlighted = searchMatchedNodeIds.has(node.id); - return { - ...node, - data: { + let nextData = searchDataCache.get(node.data); + if (!nextData || nextData.isHighlighted !== isHighlighted) { + nextData = { ...node.data, isDimmed: !isHighlighted, isHighlighted, - }, + }; + searchDataCache.set(node.data, nextData); + } + return { + ...node, + data: nextData, }; }); }, [nodes, normalizedNodeSearch, searchMatchedNodeIds]);