diff --git a/.jules/bolt.md b/.jules/bolt.md index f1a8c1466..9a89d7d23 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-08-25 - [Search Performance] 노드 텍스트 통합 및 WeakMap 캐싱을 통한 ERD 검색 최적화 +**Learning:** 다수의 문자열 필드를 개별적으로 `.toLocaleLowerCase()` 호출하여 검색하면, 검색어 입력마다 수많은 문자열 객체가 생성되어 GC 부하와 프레임 드랍이 발생합니다. V8의 단일 문자열 탐색이 JS 루프보다 훨씬 빠릅니다. +**Action:** 노드의 모든 검색 대상 텍스트를 하나의 문자열로 결합한 뒤 소문자로 변환하고, 이를 안정적인 `node.data` 객체를 키로 하는 `WeakMap`에 캐싱하여 O(1) 조회로 최적화해야 합니다. diff --git a/frontend/src/erd/search.ts b/frontend/src/erd/search.ts index 51806947a..69c3bb053 100644 --- a/frontend/src/erd/search.ts +++ b/frontend/src/erd/search.ts @@ -2,21 +2,25 @@ import type { Node } from "@xyflow/react"; import type { TableNodeData } from "./convert"; -function fieldIncludes(value: string | null | undefined, term: string): boolean { - return Boolean(value && value.toLocaleLowerCase().includes(term)); -} +const nodeSearchTextCache = new WeakMap(); + +function getNodeSearchText(node: Node): string { + let text = nodeSearchTextCache.get(node.data); + if (text !== undefined) return text; -function nodeIncludesTerm(node: Node, term: string): boolean { - if (fieldIncludes(node.data.title, term)) return true; - if (fieldIncludes(node.data.comment, term)) return true; + const parts: string[] = []; + if (node.data.title) parts.push(node.data.title); + if (node.data.comment) parts.push(node.data.comment); - for (const column of node.data.columns) { - if (fieldIncludes(column.column_name, term)) return true; - if (fieldIncludes(column.data_type, term)) return true; - if (fieldIncludes(column.column_comment, term)) return true; + for (const col of node.data.columns) { + if (col.column_name) parts.push(col.column_name); + if (col.data_type) parts.push(col.data_type); + if (col.column_comment) parts.push(col.column_comment); } - return false; + text = parts.join(" ").toLocaleLowerCase(); + nodeSearchTextCache.set(node.data, text); + return text; } export function tableNodeMatchesSearch( @@ -29,7 +33,9 @@ export function tableNodeMatchesSearch( new Set(search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)), ); if (terms.length === 0) return false; - return terms.every((term) => nodeIncludesTerm(node, term)); + + const nodeText = getNodeSearchText(node); + return terms.every((term) => nodeText.includes(term)); } export function findSearchMatchedNodeIds(