diff --git a/.jules/bolt.md b/.jules/bolt.md index edb687422..9940d1d40 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2026-10-15 - [Performance] Prevent unnecessary UI array recalculations **Learning:** In the chat component (`useThreadList`), derived state that requires filtering and sorting large arrays (like generating nested agent lists) was running synchronously on every render. Given that chat polling triggers frequent re-renders, this caused unnecessary CPU strain on lists that only rarely change. **Action:** Use `useMemo` for derived states like `rows` and `current` in custom hooks when they involve `.filter()`, `.map()`, or `.sort()` and depend on data that updates less frequently than the component renders. + +## 2026-11-06 - [Performance] CodeBlock.tsx - Avoid .map().join() overhead +**Learning:** In React/Preact components, using nested array transformations like `tokens.map(l => l.map(t => t.content).join('')).join('\n')` inside derived state on hot paths (like CodeBlock rendering) creates numerous intermediate string and array allocations. This puts significant pressure on the garbage collector and causes CPU overhead on every render, especially with large code blocks. +**Action:** Always replace `.map().join()` with manual `for` loops and string concatenation inside a `useMemo` block when deriving string values from large or nested arrays. diff --git a/apps/web/src/components/chat/CodeBlock.tsx b/apps/web/src/components/chat/CodeBlock.tsx index a86bfe958..4b9114952 100644 --- a/apps/web/src/components/chat/CodeBlock.tsx +++ b/apps/web/src/components/chat/CodeBlock.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState, useMemo } from 'preact/hooks'; import { Check, Copy } from 'lucide-preact'; import { copyText } from '../../lib/clipboard'; import { highlightCode, normalizeLang, type CodeToken } from '../../lib/highlight'; @@ -26,7 +26,24 @@ export function CodeBlock({ code, lang }: { code: string; lang?: string }) { window.clearTimeout(t); }; }, [code, lang]); - const shown = tokens && tokens.map((l) => l.map((t) => t.content).join('')).join('\n') === code ? tokens : null; + + // Performance optimization: Avoid intermediate array allocations (nested .map().join()) + // during hot UI paths for derived state. Manual loops with string concatenation are much faster + // and reduce memory overhead when dealing with potentially large tokens arrays. + const shown = useMemo(() => { + if (!tokens) return null; + let str = ''; + for (let i = 0; i < tokens.length; i++) { + const line = tokens[i]; + for (let j = 0; j < line.length; j++) { + str += line[j].content; + } + if (i < tokens.length - 1) { + str += '\n'; + } + } + return str === code ? tokens : null; + }, [tokens, code]); async function onCopy() { const ok = await copyText(code);