From d938f8fe3930e5cd405ba4546a9acef0528e13af Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:39:58 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=8B=9C=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=9C=20WeakMap=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `App.tsx`의 `visibleNodes`에서 노드 드래그(60fps) 시 불필요한 `node.data` 객체 할당을 방지하고 `React.memo` 참조를 유지하도록 `WeakMap` 캐시 적용. - `search.ts`의 `findSearchMatchedNodeIds` 검색 로직에도 `node.data` 기반의 `WeakMap` 캐시를 도입하여 드래그 중 O(N*C) 탐색 재실행 방지. - `cardinalityColumns` 의존성 배열을 객체 참조(cardinalityNode)에서 데이터 참조(cardinalityNode?.data)로 변경하여 렌더링 최적화. --- .jules/bolt.md | 3 +++ frontend/src/App.tsx | 26 +++++++++++++++++++++----- frontend/src/erd/search.ts | 25 +++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index f1a8c146..ce45a8ae 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -77,3 +77,6 @@ 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. +## 2026-07-16 - Avoid React.memo breakage in React Flow high-frequency updates +**Learning:** During high-frequency updates in React Flow (like node dragging at 60fps), `nodes` array mapping that creates a new `data` object reference (e.g., to inject `isDimmed`/`isHighlighted`) breaks the fast-path `prev.data === next.data` in `TableNode`'s `React.memo`, triggering deep re-renders across the entire graph. Additionally, O(N*C) computations (like text search over all nodes' columns) attached to the `nodes` dependency array will be redundantly re-evaluated on every frame. +**Action:** Since React Flow creates new object references for nodes when dragged but keeps the `node.data` reference stable, use a `WeakMap` keyed by `node.data` to cache derived or decorated data and search results. This preserves object identity, avoiding full re-renders and repeated heavy computations during dragging operations. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d0dc258..fc95d198 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -197,17 +197,33 @@ export default function App() { const searchMatchedNodeIds = useMemo(() => { return findSearchMatchedNodeIds(nodes, normalizedNodeSearch); }, [nodes, normalizedNodeSearch]); + + const visibleNodesDataCache = useRef(new WeakMap>()); + const visibleNodes = useMemo(() => { if (!normalizedNodeSearch) return nodes; return nodes.map((node) => { const isHighlighted = searchMatchedNodeIds.has(node.id); - return { - ...node, - data: { + + let cacheMap = visibleNodesDataCache.current.get(node.data); + if (!cacheMap) { + cacheMap = new Map(); + visibleNodesDataCache.current.set(node.data, cacheMap); + } + + let decoratedData = cacheMap.get(isHighlighted); + if (!decoratedData) { + decoratedData = { ...node.data, isDimmed: !isHighlighted, isHighlighted, - }, + }; + cacheMap.set(isHighlighted, decoratedData); + } + + return { + ...node, + data: decoratedData, }; }); }, [nodes, normalizedNodeSearch, searchMatchedNodeIds]); @@ -382,7 +398,7 @@ export default function App() { }, [ cardinalityColumnSelections, cardinalityDistinctCounts, - cardinalityNode, + cardinalityNode?.data, ]); const cardinalityRecommendations = useMemo( () => diff --git a/frontend/src/erd/search.ts b/frontend/src/erd/search.ts index 51806947..75129dc7 100644 --- a/frontend/src/erd/search.ts +++ b/frontend/src/erd/search.ts @@ -32,6 +32,11 @@ export function tableNodeMatchesSearch( return terms.every((term) => nodeIncludesTerm(node, term)); } +// ⚡ Bolt: WeakMap to cache search matches by node.data. +// Since `node.data` remains stable during drags, this avoids re-evaluating the O(N*C) search function +// across all nodes at 60fps when dragging while a search filter is active. +const searchCache = new WeakMap>(); + export function findSearchMatchedNodeIds( nodes: Array>, search: string, @@ -39,13 +44,29 @@ export function findSearchMatchedNodeIds( const matches = new Set(); // ⚡ Bolt: Parse search terms ONCE outside the loop (O(1)) instead of inside tableNodeMatchesSearch for every node (O(N)), // eliminating redundant string allocations, regex splits, and Sets per node. + const trimmedSearch = search.trim().toLocaleLowerCase(); const terms = Array.from( - new Set(search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)), + new Set(trimmedSearch.split(/\s+/).filter(Boolean)), ); if (terms.length === 0) return matches; + // Use a predictable cache key for this search. + const searchKey = terms.join(" "); + for (const node of nodes) { - if (tableNodeMatchesSearch(node, terms)) { + let nodeCache = searchCache.get(node.data); + if (!nodeCache) { + nodeCache = new Map(); + searchCache.set(node.data, nodeCache); + } + + let isMatch = nodeCache.get(searchKey); + if (isMatch === undefined) { + isMatch = tableNodeMatchesSearch(node, terms); + nodeCache.set(searchKey, isMatch); + } + + if (isMatch) { matches.add(node.id); } } From 402f2b0713458d4f334e49a616f1b61c5c36702c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:57:17 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=8B=9C=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=9C=20WeakMap=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx의 visibleNodes에서 노드 드래그(60fps) 시 불필요한 node.data 객체 할당을 방지하고 React.memo 참조를 유지하도록 WeakMap 캐시 적용. - search.ts의 findSearchMatchedNodeIds 검색 로직에도 node.data 기반의 WeakMap 캐시를 도입하여 드래그 중 O(N*C) 탐색 재실행 방지. - cardinalityColumns 의존성 배열을 객체 참조(cardinalityNode)에서 데이터 참조(cardinalityNode?.data)로 변경하여 렌더링 최적화. From c25404ce8cb515ae5c1e7e3d913bfd28743561ad Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:06:24 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=8B=9C=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=9C=20WeakMap=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx의 visibleNodes에서 노드 드래그(60fps) 시 불필요한 node.data 객체 할당을 방지하고 React.memo 참조를 유지하도록 WeakMap 캐시 적용. - search.ts의 findSearchMatchedNodeIds 검색 로직에도 node.data 기반의 WeakMap 캐시를 도입하여 드래그 중 O(N*C) 탐색 재실행 방지. - cardinalityColumns 의존성 배열을 객체 참조(cardinalityNode)에서 데이터 참조(cardinalityNode?.data)로 변경하여 렌더링 최적화. - CI 빌드 오류 해결을 위해 packageManager 명시 (pnpm@10.30.3) --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index 02439a97..af18653b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "engines": { "node": ">=26 <27" }, + "packageManager": "pnpm@10.30.3", "scripts": { "dev": "vite", "build": "tsc -b && vite build", From 320645b1bd403f0801e164e14686df7a787c30fb Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:48:33 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=8B=9C=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=9C=20WeakMap=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx의 visibleNodes에서 노드 드래그(60fps) 시 불필요한 node.data 객체 할당을 방지하고 React.memo 참조를 유지하도록 WeakMap 캐시 적용. - search.ts의 findSearchMatchedNodeIds 검색 로직에도 node.data 기반의 WeakMap 캐시를 도입하여 드래그 중 O(N*C) 탐색 재실행 방지. - cardinalityColumns 의존성 배열을 객체 참조(cardinalityNode)에서 데이터 참조(cardinalityNode?.data)로 변경하여 렌더링 최적화. --- frontend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index af18653b..02439a97 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,6 @@ "engines": { "node": ">=26 <27" }, - "packageManager": "pnpm@10.30.3", "scripts": { "dev": "vite", "build": "tsc -b && vite build",