From 369fbac116a78da97f314ef4f1c2eb9f44bbb9d2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:57:05 +0000 Subject: [PATCH] perf(ui): memoize chat thread list derivation Memoize the \`rows\` and \`current\` states in \`useThreadList\` using \`useMemo\` to prevent expensive array filtering and sorting on every render, which is particularly beneficial during chat polling. Co-authored-by: bobdivx <6737167+bobdivx@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ apps/web/src/components/chat/useThreadList.ts | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index daa16f158d..edb6874221 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2026-10-12 - [Performance] Optimize Chat Message Signature Generation **Learning:** During chat polling, \`messagesSignature\` generates a unique string for the whole thread. Using \`messages.map(...).join(',')\` nested with another \`map().join()\` created a lot of intermediate arrays and strings on every poll, causing CPU/Memory overhead on long lists. The previous signature was also very bloated because it used empty spaces for messages without reactions (e.g. \`,,,,\`). **Action:** Replace nested \`map().join()\` calls with a simple \`for\` loop and string concatenation. This is about 4-5x faster and significantly more memory efficient by skipping empty slots instead of allocating intermediate arrays. When constructing signatures or serializing large arrays on a hot path, prefer manual loops over array map/filter chains. + +## 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. diff --git a/apps/web/src/components/chat/useThreadList.ts b/apps/web/src/components/chat/useThreadList.ts index 7610d36d3f..0bb9adafbb 100644 --- a/apps/web/src/components/chat/useThreadList.ts +++ b/apps/web/src/components/chat/useThreadList.ts @@ -1,4 +1,4 @@ -import { useRef, useState } from 'preact/hooks'; +import { useRef, useState, useMemo } from 'preact/hooks'; import { api, type ProjectAgent } from '../../lib/api'; import { useToast } from '../ui'; import { nestAgentRows, readStoredThread, sortAgentsPinned, sortByRecent, storeThread, titleFromMessage } from './thread-meta'; @@ -25,10 +25,17 @@ export function useThreadList({ projectUuid, threadsMode, persona, defaultAgentU const bootstrapped = useRef(false); const list = threadsMode ? threads : agents; - const current = list.find((a) => a.uuid === selected) ?? agents.find((a) => a.uuid === selected) ?? null; - const rows = threadsMode - ? nestAgentRows(list.filter((a) => a.kind !== 'subagent'), list) - : nestAgentRows(sortAgentsPinned(agents.filter((a) => a.kind !== 'subagent')), agents); + + const current = useMemo( + () => list.find((a) => a.uuid === selected) ?? agents.find((a) => a.uuid === selected) ?? null, + [list, agents, selected] + ); + + const rows = useMemo(() => { + return threadsMode + ? nestAgentRows(list.filter((a) => a.kind !== 'subagent'), list) + : nestAgentRows(sortAgentsPinned(agents.filter((a) => a.kind !== 'subagent')), agents); + }, [threadsMode, list, agents]); async function resolveThreads(main: ProjectAgent[]): Promise { // Workspace = fil coordinateur permanent + tâches isolées. Ops/Reviewer restent dans Agents.