diff --git a/.jules/bolt.md b/.jules/bolt.md index 603b207d0..ca1af7062 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -14,3 +14,6 @@ ## 2026-07-28 - Memoize text processing in React **Learning:** Performing expensive string manipulations like splitting long texts (`transcript.split('\n')`) or generating dynamic Regex expressions inside a component body causes significant CPU overhead on every re-render (like keystroke updates in a search box). **Action:** Extract pure transformation logic on static/infrequent data into `useMemo` hooks (e.g., memoizing the paragraph split on `transcript` and precomputing search `RegExp` based on `searchQuery`). +## 2026-07-23 - Optimize string operations in React filtering loops +**Learning:** Found an inefficiency in `InteractiveTranscript.tsx` where `.toLowerCase()` on the search query was evaluated *inside* a `.filter()` loop, repeating a constant operation O(N) times. Additionally, the expensive text matching was evaluated even if the speaker filter failed. +**Action:** When filtering large arrays in React `useMemo` hooks, always hoist constant operations (like query lowercasing) outside the loop and use short-circuit evaluation (`if (!matchesPreviousCondition) return false;`) to skip expensive string methods. Also remember to add safety checks like `val ? val.toLowerCase() : ''` to avoid crashes. diff --git a/apps/web/src/components/InteractiveTranscript.tsx b/apps/web/src/components/InteractiveTranscript.tsx index 21f79d91b..6f3e03fcc 100644 --- a/apps/web/src/components/InteractiveTranscript.tsx +++ b/apps/web/src/components/InteractiveTranscript.tsx @@ -166,12 +166,22 @@ export default function InteractiveTranscript({ ); const filteredSegments = useMemo(() => { + // Optimization: Pre-compute the lowercase search query outside the loop + // to prevent recalculating it for every segment. Also short-circuit the + // expensive string matching if the speaker filter already fails. + const lowerQuery = searchQuery ? searchQuery.toLowerCase() : ''; + return segments.filter((seg) => { const matchesSpeaker = !filterSpeaker || seg.speaker === filterSpeaker; + + // Short-circuit: if speaker doesn't match, we can skip the text search + if (!matchesSpeaker) return false; + const matchesSearch = - !searchQuery || - seg.text.toLowerCase().includes(searchQuery.toLowerCase()); - return matchesSpeaker && matchesSearch; + !lowerQuery || + (seg.text ? seg.text.toLowerCase().includes(lowerQuery) : false); + + return matchesSearch; }); }, [segments, filterSpeaker, searchQuery]);