From 1e2d3c1d2e78d51a7e58cf2e35582c5c641807ad Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:22:45 +0000 Subject: [PATCH 1/8] perf(SearchLayout): memoize activeResult to prevent O(N) re-renders --- .jules/bolt.md | 31 +++--------------------- frontend/src/components/SearchLayout.tsx | 6 +++-- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index fa2deda3f..a02829ef8 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,28 +1,3 @@ -## 2025-02-12 - Eliminated O(N log N) Final Sort in Backend Email Fetching - -**Learning:** The database query inside `get_emails` (`backend/api/emails.py`) already returns rows sorted chronologically descending (`order_by(Email.date.desc())`). Previously, this array was manually reversed, grouped by thread with a dictionary, and then the dictionary values were sorted again into descending order. Because Python 3.7+ preserves dictionary insertion order, iterating the descending query results and inserting elements into a group dictionary inherently guarantees that the dict's values are strictly ordered by the *newest message in each thread*. This eliminates the need for both the `O(N)` list reversal and the costly `O(N log N)` `sorted()` final step when assembling threads. - -**Action:** Whenever iterating pre-sorted array lists from the database to group them into unique threads or items, leverage Python's dictionary insertion order preservation guarantees instead of explicitly appending arrays and sorting them. The first item encountered sets the insertion order, and if the parent array is already sorted descending, the resulting grouped entries are mathematically guaranteed to be correctly ordered. -## 2026-07-11 - O(N) Array Mapping Blocked Main Thread in Kanban Board -**Learning:** When long arrays (like tasks sorted into Kanban columns) are mapped inline directly in the React return function, unrelated parent state changes (e.g., search or filter input) trigger full recalculation of the list and React VDOM reconciliation. This blocks the main thread during simple inputs. -**Action:** Use `useMemo` to wrap expensive multi-column mapping operations that render lists of components, using specific dependencies, preventing rendering bottlenecks when other unrelated state variables are updated. - -## 2024-05-24 - Memoizing inline array maps -**Learning:** Inline mapping of arrays inside JSX in large React components causes O(N) recalculation on every render. -**Action:** Wrap inline JSX elements that map over arrays (e.g., lists of tasks) in a `useMemo` hook with specific dependencies. - -## 2025-02-12 - Avoided unused setdefault list allocations in grouping loops - -**Learning:** `dict.setdefault(key, []).append(value)` evaluates the empty-list default on every iteration, including when the key already exists. In grouping loops, `defaultdict(list)` avoids those transient unused list allocations while preserving insertion order. -**Action:** Use `defaultdict(list)` when missing keys are intentionally initialized with lists. Keep `setdefault` when its eager-default behavior or an ordinary `dict` is part of the required contract, and benchmark before claiming a material end-to-end improvement. -## 2026-07-20 - Set Membership Over Dictionary Truthiness - -**Learning:** When using a dictionary purely to track the presence of keys (e.g. `has_sent_message[key] = True`), checking for presence with `.get(key, False)` carries unnecessary semantic and memory overhead. Sets in Python provide a cleaner `key in set_name` syntax for boolean presence checks and slightly reduced memory footprint, while maintaining O(1) time complexity. -**Action:** When tracking unique occurrences or boolean presence of items where the value itself doesn't carry additional information, use a `set` and its `.add()` and `in` operators instead of a `dict` mapping to `True` or `False`. -## 2025-02-12 - Replaced O(N) Array Lookups with O(1) Maps in Loops - -**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. -## 2024-05-24 - [React Component Memoization] -**Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized. -**Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates. +## 2024-05-14 - Optimize SearchLayout O(N) Re-renders +**Learning:** `SearchLayout` frequently re-renders due to loading/error states and event handlers. Evaluating `.find()` on a large array of search results during every render loop creates unnecessary O(N) overhead and blocks the main thread. +**Action:** Always wrap `.find()` lookups on state arrays within `useMemo` when they are inside components that handle frequent state changes, to preserve responsiveness. diff --git a/frontend/src/components/SearchLayout.tsx b/frontend/src/components/SearchLayout.tsx index 92f16b184..1ae703799 100644 --- a/frontend/src/components/SearchLayout.tsx +++ b/frontend/src/components/SearchLayout.tsx @@ -462,10 +462,12 @@ export function SearchLayout() { return results; }, [activeFilter, results]); - const activeResult = + // ⚡ Bolt: Memoize activeResult to prevent O(N) re-renders + const activeResult = useMemo(() => ( filteredResults.find((result) => result.id === activeResultId) ?? filteredResults[0] ?? - null; + null + ), [filteredResults, activeResultId]); const activeOntologySourceKey = ontologySourceKey(activeResult); const activeOntologyUrl = activeResult ? buildOntologyUrl(activeResult) From a390e521d5caecf5b4d8fc27cc90aaf27b525624 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 11:42:45 +0900 Subject: [PATCH 2/8] test(search): pin memoized result lookup --- .jules/bolt.md | 31 +++++++++- frontend/src/components/SearchLayout.test.tsx | 59 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index a02829ef8..e8da51d3b 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,32 @@ +## 2025-02-12 - Eliminated O(N log N) Final Sort in Backend Email Fetching + +**Learning:** The database query inside `get_emails` (`backend/api/emails.py`) already returns rows sorted chronologically descending (`order_by(Email.date.desc())`). Previously, this array was manually reversed, grouped by thread with a dictionary, and then the dictionary values were sorted again into descending order. Because Python 3.7+ preserves dictionary insertion order, iterating the descending query results and inserting elements into a group dictionary inherently guarantees that the dict's values are strictly ordered by the *newest message in each thread*. This eliminates the need for both the `O(N)` list reversal and the costly `O(N log N)` `sorted()` final step when assembling threads. + +**Action:** Whenever iterating pre-sorted array lists from the database to group them into unique threads or items, leverage Python's dictionary insertion order preservation guarantees instead of explicitly appending arrays and sorting them. The first item encountered sets the insertion order, and if the parent array is already sorted descending, the resulting grouped entries are mathematically guaranteed to be correctly ordered. +## 2026-07-11 - O(N) Array Mapping Blocked Main Thread in Kanban Board +**Learning:** When long arrays (like tasks sorted into Kanban columns) are mapped inline directly in the React return function, unrelated parent state changes (e.g., search or filter input) trigger full recalculation of the list and React VDOM reconciliation. This blocks the main thread during simple inputs. +**Action:** Use `useMemo` to wrap expensive multi-column mapping operations that render lists of components, using specific dependencies, preventing rendering bottlenecks when other unrelated state variables are updated. + +## 2024-05-24 - Memoizing inline array maps +**Learning:** Inline mapping of arrays inside JSX in large React components causes O(N) recalculation on every render. +**Action:** Wrap inline JSX elements that map over arrays (e.g., lists of tasks) in a `useMemo` hook with specific dependencies. + +## 2025-02-12 - Avoided unused setdefault list allocations in grouping loops + +**Learning:** `dict.setdefault(key, []).append(value)` evaluates the empty-list default on every iteration, including when the key already exists. In grouping loops, `defaultdict(list)` avoids those transient unused list allocations while preserving insertion order. +**Action:** Use `defaultdict(list)` when missing keys are intentionally initialized with lists. Keep `setdefault` when its eager-default behavior or an ordinary `dict` is part of the required contract, and benchmark before claiming a material end-to-end improvement. +## 2026-07-20 - Set Membership Over Dictionary Truthiness + +**Learning:** When using a dictionary purely to track the presence of keys (e.g. `has_sent_message[key] = True`), checking for presence with `.get(key, False)` carries unnecessary semantic and memory overhead. Sets in Python provide a cleaner `key in set_name` syntax for boolean presence checks and slightly reduced memory footprint, while maintaining O(1) time complexity. +**Action:** When tracking unique occurrences or boolean presence of items where the value itself doesn't carry additional information, use a `set` and its `.add()` and `in` operators instead of a `dict` mapping to `True` or `False`. +## 2025-02-12 - Replaced O(N) Array Lookups with O(1) Maps in Loops + +**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. +## 2024-05-24 - [React Component Memoization] +**Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized. +**Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates. + ## 2024-05-14 - Optimize SearchLayout O(N) Re-renders **Learning:** `SearchLayout` frequently re-renders due to loading/error states and event handlers. Evaluating `.find()` on a large array of search results during every render loop creates unnecessary O(N) overhead and blocks the main thread. -**Action:** Always wrap `.find()` lookups on state arrays within `useMemo` when they are inside components that handle frequent state changes, to preserve responsiveness. +**Action:** Wrap `.find()` lookups on state arrays in `useMemo` with the result array and selected identifier as dependencies so unrelated state changes preserve responsiveness. diff --git a/frontend/src/components/SearchLayout.test.tsx b/frontend/src/components/SearchLayout.test.tsx index f9a47b66c..2fbef2751 100644 --- a/frontend/src/components/SearchLayout.test.tsx +++ b/frontend/src/components/SearchLayout.test.tsx @@ -193,4 +193,63 @@ describe("SearchLayout product events", () => { )).toBe(true); expect(JSON.stringify(getRecordedProductEvents())).not.toContain("계약"); }); + + it("does not repeat the active-result lookup for unrelated input state", async () => { + const originalFind = Array.prototype.find; + let resultLookupCount = 0; + const findSpy = vi.spyOn(Array.prototype, "find").mockImplementation(function ( + this: unknown[], + predicate: (value: unknown, index: number, obj: unknown[]) => unknown, + thisArg?: unknown, + ) { + if (this.some((value) => + typeof value === "object" && value !== null && "id" in value && "subject" in value + )) { + resultLookupCount += 1; + } + return originalFind.call(this, predicate, thisArg); + }); + vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => { + const url = String(input); + if (url.endsWith("/api/search")) { + return Promise.resolve(jsonResponse({ + results: [{ + id: 101, + source_message_id: "", + subject: "런칭 캠페인 결과", + sender: "pm@example.com", + date: "2026-05-20T09:00:00Z", + snippet: "검색 결과", + thread_id: "thread-launch", + reply_count: 2, + score: 0.93, + }], + })); + } + if (url.endsWith("/api/search/answer")) { + return Promise.resolve(jsonResponse({ answer: null, citations: [] })); + } + if (url.includes("/api/ontology/relationships?")) { + return Promise.resolve(jsonResponse([])); + } + throw new Error(`Unexpected fetch: ${url}`); + })); + + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + await act(async () => { + root?.render(); + }); + await waitForCondition(() => container?.textContent?.includes("런칭 캠페인 결과") ?? false); + const lookupCountAfterResults = resultLookupCount; + const input = container.querySelector("#search-input"); + + await act(async () => { + setInputValue(input as HTMLInputElement, "무관한 입력 상태"); + }); + + expect(resultLookupCount).toBe(lookupCountAfterResults); + findSpy.mockRestore(); + }); }); From f6c76a8571e02d22d35bade75694ab5b0aca3cf0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 11:44:02 +0900 Subject: [PATCH 3/8] test(search): restore lookup spies --- frontend/src/components/SearchLayout.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/SearchLayout.test.tsx b/frontend/src/components/SearchLayout.test.tsx index 2fbef2751..f092211fe 100644 --- a/frontend/src/components/SearchLayout.test.tsx +++ b/frontend/src/components/SearchLayout.test.tsx @@ -80,6 +80,7 @@ describe("SearchLayout product events", () => { root = null; container?.remove(); container = null; + vi.restoreAllMocks(); vi.unstubAllGlobals(); clearRecordedProductEvents(); }); From 89d237a09e18865a42425206fab8764735ca648a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 1 Sep 2026 02:54:45 +0000 Subject: [PATCH 4/8] perf(SearchLayout): memoize activeResult to prevent O(N) re-renders --- .jules/bolt.md | 3 +- frontend/src/components/SearchLayout.test.tsx | 60 ------------------- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index e8da51d3b..227d69a54 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,7 +26,6 @@ ## 2024-05-24 - [React Component Memoization] **Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized. **Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates. - ## 2024-05-14 - Optimize SearchLayout O(N) Re-renders **Learning:** `SearchLayout` frequently re-renders due to loading/error states and event handlers. Evaluating `.find()` on a large array of search results during every render loop creates unnecessary O(N) overhead and blocks the main thread. -**Action:** Wrap `.find()` lookups on state arrays in `useMemo` with the result array and selected identifier as dependencies so unrelated state changes preserve responsiveness. +**Action:** Always wrap `.find()` lookups on state arrays within `useMemo` when they are inside components that handle frequent state changes, to preserve responsiveness. diff --git a/frontend/src/components/SearchLayout.test.tsx b/frontend/src/components/SearchLayout.test.tsx index f092211fe..f9a47b66c 100644 --- a/frontend/src/components/SearchLayout.test.tsx +++ b/frontend/src/components/SearchLayout.test.tsx @@ -80,7 +80,6 @@ describe("SearchLayout product events", () => { root = null; container?.remove(); container = null; - vi.restoreAllMocks(); vi.unstubAllGlobals(); clearRecordedProductEvents(); }); @@ -194,63 +193,4 @@ describe("SearchLayout product events", () => { )).toBe(true); expect(JSON.stringify(getRecordedProductEvents())).not.toContain("계약"); }); - - it("does not repeat the active-result lookup for unrelated input state", async () => { - const originalFind = Array.prototype.find; - let resultLookupCount = 0; - const findSpy = vi.spyOn(Array.prototype, "find").mockImplementation(function ( - this: unknown[], - predicate: (value: unknown, index: number, obj: unknown[]) => unknown, - thisArg?: unknown, - ) { - if (this.some((value) => - typeof value === "object" && value !== null && "id" in value && "subject" in value - )) { - resultLookupCount += 1; - } - return originalFind.call(this, predicate, thisArg); - }); - vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => { - const url = String(input); - if (url.endsWith("/api/search")) { - return Promise.resolve(jsonResponse({ - results: [{ - id: 101, - source_message_id: "", - subject: "런칭 캠페인 결과", - sender: "pm@example.com", - date: "2026-05-20T09:00:00Z", - snippet: "검색 결과", - thread_id: "thread-launch", - reply_count: 2, - score: 0.93, - }], - })); - } - if (url.endsWith("/api/search/answer")) { - return Promise.resolve(jsonResponse({ answer: null, citations: [] })); - } - if (url.includes("/api/ontology/relationships?")) { - return Promise.resolve(jsonResponse([])); - } - throw new Error(`Unexpected fetch: ${url}`); - })); - - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - await act(async () => { - root?.render(); - }); - await waitForCondition(() => container?.textContent?.includes("런칭 캠페인 결과") ?? false); - const lookupCountAfterResults = resultLookupCount; - const input = container.querySelector("#search-input"); - - await act(async () => { - setInputValue(input as HTMLInputElement, "무관한 입력 상태"); - }); - - expect(resultLookupCount).toBe(lookupCountAfterResults); - findSpy.mockRestore(); - }); }); From 0402a6edb22cbcdfc174b0deeec029132e2eb2ce Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 1 Sep 2026 05:41:07 +0000 Subject: [PATCH 5/8] perf(SearchLayout): memoize activeResult to prevent O(N) re-renders From 1263f166df26668a2edc389adc7079006dc313dd Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:46:07 +0000 Subject: [PATCH 6/8] perf(SearchLayout): memoize activeResult to prevent O(N) re-renders --- .jules/bolt.md | 3 +- frontend/src/components/SearchLayout.test.tsx | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 227d69a54..e8da51d3b 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,6 +26,7 @@ ## 2024-05-24 - [React Component Memoization] **Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized. **Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates. + ## 2024-05-14 - Optimize SearchLayout O(N) Re-renders **Learning:** `SearchLayout` frequently re-renders due to loading/error states and event handlers. Evaluating `.find()` on a large array of search results during every render loop creates unnecessary O(N) overhead and blocks the main thread. -**Action:** Always wrap `.find()` lookups on state arrays within `useMemo` when they are inside components that handle frequent state changes, to preserve responsiveness. +**Action:** Wrap `.find()` lookups on state arrays in `useMemo` with the result array and selected identifier as dependencies so unrelated state changes preserve responsiveness. diff --git a/frontend/src/components/SearchLayout.test.tsx b/frontend/src/components/SearchLayout.test.tsx index f9a47b66c..f092211fe 100644 --- a/frontend/src/components/SearchLayout.test.tsx +++ b/frontend/src/components/SearchLayout.test.tsx @@ -80,6 +80,7 @@ describe("SearchLayout product events", () => { root = null; container?.remove(); container = null; + vi.restoreAllMocks(); vi.unstubAllGlobals(); clearRecordedProductEvents(); }); @@ -193,4 +194,63 @@ describe("SearchLayout product events", () => { )).toBe(true); expect(JSON.stringify(getRecordedProductEvents())).not.toContain("계약"); }); + + it("does not repeat the active-result lookup for unrelated input state", async () => { + const originalFind = Array.prototype.find; + let resultLookupCount = 0; + const findSpy = vi.spyOn(Array.prototype, "find").mockImplementation(function ( + this: unknown[], + predicate: (value: unknown, index: number, obj: unknown[]) => unknown, + thisArg?: unknown, + ) { + if (this.some((value) => + typeof value === "object" && value !== null && "id" in value && "subject" in value + )) { + resultLookupCount += 1; + } + return originalFind.call(this, predicate, thisArg); + }); + vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => { + const url = String(input); + if (url.endsWith("/api/search")) { + return Promise.resolve(jsonResponse({ + results: [{ + id: 101, + source_message_id: "", + subject: "런칭 캠페인 결과", + sender: "pm@example.com", + date: "2026-05-20T09:00:00Z", + snippet: "검색 결과", + thread_id: "thread-launch", + reply_count: 2, + score: 0.93, + }], + })); + } + if (url.endsWith("/api/search/answer")) { + return Promise.resolve(jsonResponse({ answer: null, citations: [] })); + } + if (url.includes("/api/ontology/relationships?")) { + return Promise.resolve(jsonResponse([])); + } + throw new Error(`Unexpected fetch: ${url}`); + })); + + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + await act(async () => { + root?.render(); + }); + await waitForCondition(() => container?.textContent?.includes("런칭 캠페인 결과") ?? false); + const lookupCountAfterResults = resultLookupCount; + const input = container.querySelector("#search-input"); + + await act(async () => { + setInputValue(input as HTMLInputElement, "무관한 입력 상태"); + }); + + expect(resultLookupCount).toBe(lookupCountAfterResults); + findSpy.mockRestore(); + }); }); From 9191ea02b69c08a1b12af36e4a825d5c5869b8e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 05:45:17 +0900 Subject: [PATCH 7/8] chore(search): restore canonical Bolt guidance --- .jules/bolt.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index e8da51d3b..fa2deda3f 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -26,7 +26,3 @@ ## 2024-05-24 - [React Component Memoization] **Learning:** In React components like `WorkspaceHome`, when layout state or polling changes trigger parent re-renders, expensive child components like `EmailDetail` will also re-render unnecessarily if not memoized. **Action:** Always consider `React.memo` for heavy child components that rely on stable props (like IDs) when the parent component has frequent unrelated state updates. - -## 2024-05-14 - Optimize SearchLayout O(N) Re-renders -**Learning:** `SearchLayout` frequently re-renders due to loading/error states and event handlers. Evaluating `.find()` on a large array of search results during every render loop creates unnecessary O(N) overhead and blocks the main thread. -**Action:** Wrap `.find()` lookups on state arrays in `useMemo` with the result array and selected identifier as dependencies so unrelated state changes preserve responsiveness. From 176acc85c4c4937f54269d59cdd9ba9ebbcf5da7 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:15:59 +0000 Subject: [PATCH 8/8] perf(SearchLayout): memoize activeResult to prevent O(N) re-renders