From f3f874bdde863b22f86f0eff379874631c125852 Mon Sep 17 00:00:00 2001
From: iotserver24 <147928812+iotserver24@users.noreply.github.com>
Date: Tue, 19 May 2026 16:51:09 +0000
Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[performance=20improvement]?=
=?UTF-8?q?=20optimize=20chat=20history=20rendering?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
💡 What: Refactored the `ChatHistory` component to use a single O(N) pass for session grouping instead of an O(4N) pass. Extracted the list items into a `ChatHistoryItem` component wrapped with `React.memo` and stabilized the `handleDelete` function with `useCallback` to prevent cascading VDOM re-renders.
🎯 Why: In a long list of chat sessions, any change in `activeSessionId` would trigger an O(4N) array processing step, re-evaluate numerous date strings, and cause the entire DOM list to re-render.
📊 Impact: Reduces array iteration from 4N to N on chat session refresh. Drops list item re-renders down from N to 2 when switching between active sessions.
🔬 Measurement: Verify by adding `console.log` into `ChatHistoryItem` or monitoring the React DevTools profiler while clicking between different sessions in the Chat History sidebar. Expect only two re-renders per switch (the old active session and the new one).
---
.jules/bolt.md | 3 +
.../src/renderer/components/ChatHistory.tsx | 128 ++++++++++++------
plan_review_request.md | 6 +
3 files changed, 92 insertions(+), 45 deletions(-)
create mode 100644 plan_review_request.md
diff --git a/.jules/bolt.md b/.jules/bolt.md
index 427be48..88f0402 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -67,3 +67,6 @@
## 2025-05-17 - Replace O(N²) array search with O(N) Set lookup in session listing
**Learning:** Using `array.some(...)` with string operations (like `replace()`) inside a loop over files creates an unnecessary O(N²) performance bottleneck, particularly when reading directories with many files (like the sessions history directory).
**Action:** When filtering files against another list of files, pre-compute a `Set` of base names outside the loop for O(1) lookups, changing the overall time complexity from O(N²) to O(N).
+## 2025-02-23 - Memoizing List Items Requires Stable Callback Props
+**Learning:** Extracting an inline list item into a `React.memo` component (like `ChatHistoryItem`) to prevent O(N) VDOM re-renders is completely ineffective if the event handler props (like `onDelete`) are not wrapped in `useCallback` in the parent component. Without `useCallback`, the parent creates a new function reference on every render, causing `React.memo`'s shallow comparison to fail and re-render every item anyway.
+**Action:** When extracting React components into `React.memo()` to prevent unnecessary re-renders (e.g., list items), ensure all functions passed as props (such as event handlers) are wrapped in `useCallback` in the parent component to maintain referential equality across renders.
diff --git a/packages/desktop/src/renderer/components/ChatHistory.tsx b/packages/desktop/src/renderer/components/ChatHistory.tsx
index 3f08573..60c1a94 100644
--- a/packages/desktop/src/renderer/components/ChatHistory.tsx
+++ b/packages/desktop/src/renderer/components/ChatHistory.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback, memo } from 'react';
+import { useState, useEffect, useCallback, useMemo, memo } from 'react';
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
import { cn } from '../lib/utils';
@@ -31,6 +31,45 @@ function relativeTime(iso: string): string {
return new Date(iso).toLocaleDateString();
}
+const ChatHistoryItem = memo(function ChatHistoryItem({
+ session,
+ isActive,
+ onSelect,
+ onDelete,
+}: {
+ session: SessionItem;
+ isActive: boolean;
+ onSelect: (id: string) => void;
+ onDelete: (e: React.MouseEvent, id: string) => void;
+}) {
+ return (
+
+ );
+});
+
const ChatHistory = memo(function ChatHistory({ activeSessionId, onSelectSession, onNewChat }: ChatHistoryProps) {
const [sessions, setSessions] = useState([]);
@@ -41,29 +80,46 @@ const ChatHistory = memo(function ChatHistory({ activeSessionId, onSelectSession
useEffect(() => { refresh(); }, [refresh]);
- const handleDelete = async (e: React.MouseEvent, id: string) => {
+ const handleDelete = useCallback(async (e: React.MouseEvent, id: string) => {
e.stopPropagation();
await xibe.session.delete(id);
refresh();
- };
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- const yesterday = new Date(today);
- yesterday.setDate(yesterday.getDate() - 1);
- const weekAgo = new Date(today);
- weekAgo.setDate(weekAgo.getDate() - 7);
-
- const groups: { label: string; items: SessionItem[] }[] = [];
- const todayItems = sessions.filter((s) => new Date(s.updated) >= today);
- const yesterdayItems = sessions.filter((s) => { const d = new Date(s.updated); return d >= yesterday && d < today; });
- const weekItems = sessions.filter((s) => { const d = new Date(s.updated); return d >= weekAgo && d < yesterday; });
- const olderItems = sessions.filter((s) => new Date(s.updated) < weekAgo);
-
- if (todayItems.length) groups.push({ label: 'Today', items: todayItems });
- if (yesterdayItems.length) groups.push({ label: 'Yesterday', items: yesterdayItems });
- if (weekItems.length) groups.push({ label: 'This Week', items: weekItems });
- if (olderItems.length) groups.push({ label: 'Older', items: olderItems });
+ }, [refresh]);
+
+ // âš¡ Bolt: Memoize the grouping of sessions into an O(N) single pass to avoid O(4N) filtering and Date parsing on every render
+ const groups = useMemo(() => {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+ const weekAgo = new Date(today);
+ weekAgo.setDate(weekAgo.getDate() - 7);
+
+ const todayItems: SessionItem[] = [];
+ const yesterdayItems: SessionItem[] = [];
+ const weekItems: SessionItem[] = [];
+ const olderItems: SessionItem[] = [];
+
+ for (const s of sessions) {
+ const d = new Date(s.updated);
+ if (d >= today) {
+ todayItems.push(s);
+ } else if (d >= yesterday) {
+ yesterdayItems.push(s);
+ } else if (d >= weekAgo) {
+ weekItems.push(s);
+ } else {
+ olderItems.push(s);
+ }
+ }
+
+ const g: { label: string; items: SessionItem[] }[] = [];
+ if (todayItems.length) g.push({ label: 'Today', items: todayItems });
+ if (yesterdayItems.length) g.push({ label: 'Yesterday', items: yesterdayItems });
+ if (weekItems.length) g.push({ label: 'This Week', items: weekItems });
+ if (olderItems.length) g.push({ label: 'Older', items: olderItems });
+ return g;
+ }, [sessions]);
return (
diff --git a/plan_review_request.md b/plan_review_request.md
new file mode 100644
index 0000000..c2728f6
--- /dev/null
+++ b/plan_review_request.md
@@ -0,0 +1,6 @@
+I plan to optimize `packages/desktop/src/renderer/components/ChatHistory.tsx` to improve performance for long lists of chat sessions.
+
+1. **Memoize the grouping of sessions into an O(N) single pass.** Currently, it runs `sessions.filter` four times, resulting in an O(4N) operation that also instantiates `new Date(s.updated)` four times per item. I will replace this with a single pass using `useMemo` that evaluates each session's date once and pushes it to the correct group, preventing expensive recalculations on every render.
+2. **Extract `ChatHistoryItem` as a `React.memo` component.** Currently, when `activeSessionId` changes, the entire list of `button` components re-renders because they are defined inline inside the mapping. By extracting the item to a `React.memo` component, only the previously active item and the newly active item will re-render, preventing O(N) cascading VDOM re-renders for unchanged chat history items.
+
+This optimization addresses "Missing memoization for expensive computations", "Inefficient algorithms (O(n²) that could be O(n))", and "Unnecessary re-renders in React components".