diff --git a/.jules/bolt.md b/.jules/bolt.md index edb687422..abc982b7a 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-10-18 - [Performance] Optimize CodeBlock tokens serialization during streaming +**Learning:** During chat streaming, the `code` prop updates frequently while `tokens` trails behind due to debouncing. The reactive fallback check `tokens.map(l => l.map(t => t.content).join('')).join('\n') === code` inside `CodeBlock.tsx` was firing on every keystroke render. This deep nested array mapping created massive intermediate array allocations and garbage collection overhead, leading to noticeable FPS drops during streaming on large code blocks. +**Action:** Always wrap heavy data serialization steps in `useMemo`, and replace nested `Array.prototype.map().join()` calls with straightforward `for` loop string concatenations when stringifying large recursive structures on hot paths to minimize temporary allocations. diff --git a/apps/web/src/components/chat/CodeBlock.tsx b/apps/web/src/components/chat/CodeBlock.tsx index a86bfe958..f1b6ae776 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,19 @@ 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; + + const shown = useMemo(() => { + if (!tokens) return null; + let text = ''; + for (let i = 0; i < tokens.length; i++) { + const line = tokens[i]; + for (let j = 0; j < line.length; j++) { + text += line[j].content; + } + if (i < tokens.length - 1) text += '\n'; + } + return text === code ? tokens : null; + }, [tokens, code]); async function onCopy() { const ok = await copyText(code);