Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@

**Learning:** When generating derived UI state in `useMemo` that joins separate data arrays (like graph edges referencing node IDs), calling helper functions that use `Array.prototype.find()` for every item creates an `O(M * N)` bottleneck.
**Action:** When a loop needs to repeatedly look up related items from another array by ID, pre-compute an `O(N)` `Map` before the loop and use `map.get()` for `O(1)` lookups instead of inline array `.find()` calls.
## 2026-07-25 - Prevent O(M*N) Bottlenecks with O(1) Maps in React Components
**Learning:** When a React component (like `NetworkGraph.tsx`) iterates or handles events that require repeatedly looking up related items from another array by ID (e.g., finding an edge by ID using `edges.find()`), it introduces `O(N)` complexity per lookup. In interactive components or large loops, this results in an `O(M * N)` bottleneck, causing frame drops and main thread blocking.
**Action:** Pre-compute an `O(N)` `Map` inside a `useMemo` hook using the source arrays (e.g., `edges`, `nodes`). Then, replace the inline `Array.prototype.find()` calls with `map.get()` to achieve `O(1)` lookups. Ensure the `useMemo` hooks and any downstream `useEffect` hooks declare the correct dependency arrays.
31 changes: 25 additions & 6 deletions frontend/src/components/NetworkGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,25 @@ export default function NetworkGraph() {
return map;
}, [nodes]);

// ⚡ Bolt: Precompute O(N) Maps for O(1) edge and node lookups to prevent main thread blocking during rapid graph interactions
const edgeObjectMap = useMemo(() => {
const map = new Map<string, Edge>();
for (const edge of edges) {
if (edge.id != null) {
map.set(String(edge.id), edge);
}
}
return map;
}, [edges]);

const nodeObjectMap = useMemo(() => {
const map = new Map<string, Node>();
for (const node of nodes) {
map.set(String(node.id), node);
}
return map;
}, [nodes]);
Comment on lines +174 to +190

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve first-match selection for colliding IDs.

The previous edges.find() and nodes.find() calls returned the first string-equivalent ID. These maps overwrite earlier entries. A 1 and "1", or duplicate backend IDs, now select the last object. nodeMap keeps the first label while nodeObjectMap selects the last node, so the label and selected node can disagree.

Keep the first map entry, or reject duplicate normalized IDs during normalization.

Proposed fix
   for (const edge of edges) {
     if (edge.id != null) {
-      map.set(String(edge.id), edge);
+      const key = String(edge.id);
+      if (!map.has(key)) {
+        map.set(key, edge);
+      }
     }
   }

Apply the same map.has(key) guard to nodeObjectMap.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const edgeObjectMap = useMemo(() => {
const map = new Map<string, Edge>();
for (const edge of edges) {
if (edge.id != null) {
map.set(String(edge.id), edge);
}
}
return map;
}, [edges]);
const nodeObjectMap = useMemo(() => {
const map = new Map<string, Node>();
for (const node of nodes) {
map.set(String(node.id), node);
}
return map;
}, [nodes]);
const edgeObjectMap = useMemo(() => {
const map = new Map<string, Edge>();
for (const edge of edges) {
if (edge.id != null) {
const key = String(edge.id);
if (!map.has(key)) {
map.set(key, edge);
}
}
}
return map;
}, [edges]);
const nodeObjectMap = useMemo(() => {
const map = new Map<string, Node>();
for (const node of nodes) {
const key = String(node.id);
if (!map.has(key)) {
map.set(key, node);
}
}
return map;
}, [nodes]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/NetworkGraph.tsx` around lines 174 - 190, Update the
nodeObjectMap construction in NetworkGraph to preserve the first node for each
normalized string ID by inserting only when the map does not already contain the
key. Apply the same first-entry behavior as edgeObjectMap or nodeMap so
duplicate and string-equivalent IDs cannot overwrite earlier selections.


useEffect(() => {
apiClient.get<NetworkData>('/api/network/graph')
.then((data) => {
Expand Down Expand Up @@ -202,7 +221,7 @@ export default function NetworkGraph() {
};

const selectEdge = (edgeId: number | string) => {
const edge = edges.find((candidate) => graphIdEquals(candidate.id, edgeId));
const edge = edgeObjectMap.get(String(edgeId));
if (!edge) return;
setRelationshipOptionId(String(edge.id));
setNodeOptionId('');
Expand All @@ -213,7 +232,7 @@ export default function NetworkGraph() {
const selectNode = (nodeId: number | string) => {
setRelationshipOptionId('');
setNodeOptionId(String(nodeId));
setSelectedGraphDetail(`선택된 노드: ${findNodeLabel(nodes, nodeId)}`);
setSelectedGraphDetail(`선택된 노드: ${nodeMap.get(String(nodeId)) ?? String(nodeId)}`);
setGraphActionStatus('그래프에서 노드를 선택했습니다.');
};

Expand Down Expand Up @@ -262,7 +281,7 @@ export default function NetworkGraph() {
network.destroy();
};
}
}, [nodes, edges, nodeMap]);
}, [nodes, edges, nodeMap, edgeObjectMap]);

const nodeLabels = useMemo(() => {
return nodes
Expand Down Expand Up @@ -303,7 +322,7 @@ export default function NetworkGraph() {
if (!isGraphId(node.id)) return;
setRelationshipOptionId('');
setNodeOptionId(String(node.id));
setSelectedGraphDetail(`선택된 노드: ${findNodeLabel(nodes, node.id)}`);
setSelectedGraphDetail(`선택된 노드: ${nodeMap.get(String(node.id)) ?? String(node.id)}`);
setGraphActionStatus(status);
networkRef.current?.selectNodes?.([node.id]);
networkRef.current?.fit?.({ nodes: [node.id], animation: false });
Expand All @@ -315,13 +334,13 @@ export default function NetworkGraph() {
};

const handleRelationshipOptionChange = (value: string) => {
const edge = edges.find((candidate) => String(candidate.id) === value);
const edge = edgeObjectMap.get(value);
if (!edge) return;
selectRelationship(edge, '선택한 관계를 열었습니다.');
};

const handleNodeOptionChange = (value: string) => {
const node = nodes.find((candidate) => String(candidate.id) === value);
const node = nodeObjectMap.get(value);
if (!node) return;
selectGraphNode(node, '선택한 노드를 열었습니다.');
};
Expand Down
Loading
Loading