Skip to content

⚡ Bolt: [performance improvement] O(N) React re-render fix in ChatHistory#98

Merged
iotserver24 merged 1 commit into
mainfrom
bolt/chat-history-optimization-7860790471269724949
May 21, 2026
Merged

⚡ Bolt: [performance improvement] O(N) React re-render fix in ChatHistory#98
iotserver24 merged 1 commit into
mainfrom
bolt/chat-history-optimization-7860790471269724949

Conversation

@iotserver24
Copy link
Copy Markdown
Owner

@iotserver24 iotserver24 commented May 21, 2026

💡 What:
Extracted the chat history session list rendering into a standalone ChatHistoryItem component wrapped with React.memo(). A pre-computed boolean isActive flag is passed down from the parent instead of the child computing its active state.

🎯 Why:
Previously, selecting a session via activeSessionId would cause all session items in the sidebar to re-render, resulting in O(N) VDOM comparisons where N is the number of chat sessions in the user's history. By delegating the render responsibility to a memoized component, we bypass this.

📊 Impact:
Reduces React re-renders from O(N) to O(1) (or O(2)) during chat selection. Only the newly selected item and the previously selected item re-render. This will significantly eliminate micro-stuttering in the Desktop UI when managing large histories.

🔬 Measurement:
Run pnpm dev in packages/desktop and click around chat items. Use React Profiler or visually inspect performance. Re-rendering will clearly highlight just the interacted nodes instead of the entire sidebar list. Tests run perfectly.


PR created automatically by Jules for task 7860790471269724949 started by @iotserver24

Summary by CodeRabbit

  • Refactor

    • Optimized chat history session rendering to improve application performance.
  • Documentation

    • Updated development documentation regarding performance considerations.

Review Change Stack

…renders

Extracted the inline button map in `ChatHistory.tsx` into a separate `ChatHistoryItem` component wrapped in `React.memo()`.

Instead of causing O(N) re-renders for the entire list when a single chat is selected, we now pass a computed boolean flag (`isActive={activeSessionId === s.id}`) down to the child component. This effectively reduces re-rendering down to O(1) where only the old active item and the new active item update, significantly improving performance when navigating history.

The `handleDelete` function in `ChatHistory` was also wrapped in a `useCallback` to prevent breaking the referential equality of props passed down to `ChatHistoryItem`.
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@xibe-review
Copy link
Copy Markdown

xibe-review Bot commented May 21, 2026

Hey @iotserver24! 👋

I'll go through the changes and help you out with an automated review! 🔍

Starting the review now...

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c2792e29-043e-4834-936d-7da9e610605d

📥 Commits

Reviewing files that changed from the base of the PR and between 82b58b5 and 19b86c5.

📒 Files selected for processing (2)
  • .jules/bolt.md
  • packages/desktop/src/renderer/components/ChatHistory.tsx

📝 Walkthrough

Walkthrough

ChatHistory component is refactored to prevent O(N) re-renders: session rows are now rendered via a memoized ChatHistoryItem component instead of inline markup, the delete handler is memoized with useCallback, and the optimization pattern is documented in the project's bolt notes.

Changes

ChatHistory React re-render optimization

Layer / File(s) Summary
Memoized component extraction and handler optimization
packages/desktop/src/renderer/components/ChatHistory.tsx
New ChatHistoryItem component (memoized) encapsulates per-session row UI with group-hover delete control and timestamp. The handleDelete function is memoized with useCallback. Parent's session list mapping now passes session, isActive, onSelectSession, and handleDelete to each ChatHistoryItem instead of inlining button markup.
Optimization pattern documentation
.jules/bolt.md
Dated lesson entry (2023-10-27) describing the O(N) re-render issue in React list components and the solution: memoize list item elements with React.memo and pass simple boolean isActive props to prevent widespread VDOM updates.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • iotserver24/Xibecode#60: Applies React.memo to memoize chat-list item components (MessageItem in ChatPanel/index.tsx) for the same re-render prevention pattern.
  • iotserver24/Xibecode#69: Directly related refactor of ChatHistory.tsx to reduce re-renders through memoized components and group-hover hover-state management.

Poem

🐰 A list that rendered, row by row so slow—
Till memo wrapped each item in its glow.
The delete button hides till hover's call,
And O(N) dwindles, no re-renders at all! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt/chat-history-optimization-7860790471269724949

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@xibe-review
Copy link
Copy Markdown

xibe-review Bot commented May 21, 2026

🔍 Code Review for @iotserver24

Great PR! The core optimization (extracting a memoized ChatHistoryItem and passing isActive pre-computed) is exactly the right pattern to eliminate O(N) re-renders. Below I cover security (none found), evaluate whether the performance gain fully materializes, and suggest two concrete improvements.


🔒 Security Concerns

None.

  • No hardcoded secrets, tokens, or credentials.
  • session.title is rendered as text inside a <button> → React’s default escaping prevents XSS.
  • No risky APIs (e.g., eval, dangerouslySetInnerHTML) introduced.

💡 User-Specific Analysis (Performance Goal)

You want to reduce re-renders from O(N) to O(1) during session selection. The approach is correct in spirit, but two details could prevent reaching O(1) in practice:

1. React.memo Shallow Compare on session Object

File: ChatHistory.tsx, lines 33–63 (component definition)
Risk: If the parent sessions array is rebuilt on every state change (e.g., refresh() returns new objects), every memoized item will re-render anyway because session is a new reference.

// Current – shallow compare fails when session object identity changes
const ChatHistoryItem = memo(function ChatHistoryItem({ session, isActive, onSelect, onDelete }) {
  // ...
});

Why this matters:
The performance win becomes O(N) again if sessions aren’t referentially stable. This is a common gotcha – the memo only helps when props are truly unchanged.

Fix:
Add a custom comparator that checks session.id (or deep equality if payload is small):

const ChatHistoryItem = memo(
  function ChatHistoryItem({ session, isActive, onSelect, onDelete }) {
    // ... implementation
  },
  (prevProps, nextProps) =>
    prevProps.session.id === nextProps.session.id &&
    prevProps.isActive === nextProps.isActive &&
    prevProps.onSelect === nextProps.onSelect &&
    prevProps.onDelete === nextProps.onDelete
);

Alternatively, ensure the parent sessions are memoized with useMemo using a stable key from the backend.

2. relativeTime(...) Runs on Every Render (Minor)

File: ChatHistory.tsx, line 55 (inside ChatHistoryItem)

<span className="...">{relativeTime(session.updated)}</span>

Impact:
Cheap per call, but scales linearly with N. If the list is large (>100 items), it adds unnecessary work.

Fix:
Wrap with useMemo:

const formattedTime = useMemo(() => relativeTime(session.updated), [session.updated]);

🔧 Recommended Changes

Priority 1: Ensure React.memo Actually Works (Custom Comparator)

File: ChatHistory.tsx, around line 33
Action: Add the comparator shown above.

Priority 2: Cache the Relative Time String

File: ChatHistory.tsx, around line 55
Action: Replace the inline call with useMemo.

Priority 3 (Non-Code): Fix Date in .jules/bolt.md

File: .jules/bolt.md, line 70
Action: Change 2023-10-27 to 2025-10-27 for consistency with the rest of the file.


✅ Comprehensive Analysis

Functionality

No functional regression. The extracted ChatHistoryItem is a pure component that renders a session title, delete button, and formatted time. The isActive flag is correctly computed upfront, and handleDelete is properly useCallback’d.

Performance

  • Correct idea: Extracting + React.memo reduces re-renders from O(N) to O(2) when only isActive changes.
  • Risk: If upstream sessions objects are not stable, the gain disappears. The PR should include verifying that the parent code (not changed here) produces stable references.
  • Minor gain: Memoizing relativeTime prevents a function call per render per item.

Maintainability

  • Very good: The component is small, focused, and well-typed.
  • The CSS-only group-hover trick for the delete button is a nice touch – avoids react state toggles.
  • The delete handler’s stopPropagation and e.preventDefault are correctly placed (prevents unwanted selection on delete click).

Code Quality

  • TypeScript: Props are properly typed. key={session.id} is correctly placed on the loop.
  • Linting: No issues in the diff.
  • One minor inconsistency: The addition in .jules/bolt.md uses a wrong year (2023 vs 2025). Not breaking, but sloppy for documentation.

📊 Review Summary

Files reviewed: 2
Issues found:

  • 🔴 Critical security: 0
  • 🟡 Warnings: 2 (memo comparator, un-memoized time)
  • 🔵 Suggestions: 1 (date fix in .jules/bolt.md)

Recommendation: ⚠️ Request changes
The approach is sound, but without a custom comparator the O(1) performance goal might not be reached. Please add the comparator and the useMemo for relativeTime, then the PR will be solid.

Final Verdict: A well-written, focused performance fix – just needs two small adjustments to guarantee the intended optimization. Great work @iotserver24!


🤖 Powered by Xibe AI • Auto-generated
📊 Analysis: 5663 characters analyzed across 2 files
💙 Real-time Analytics📚 Documentation

@iotserver24 iotserver24 marked this pull request as ready for review May 21, 2026 16:49
Copilot AI review requested due to automatic review settings May 21, 2026 16:49
@iotserver24 iotserver24 merged commit ceebbc9 into main May 21, 2026
5 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR refactors the chat history list to reduce per-item re-renders by extracting a memoized ChatHistoryItem component and stabilizing the delete handler.

Changes:

  • Extracted a React.memo’d ChatHistoryItem component to render individual sessions.
  • Memoized handleDelete via useCallback and passed it into list items.
  • Added a Bolt learnings note about avoiding O(N) list re-renders in React.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packages/desktop/src/renderer/components/ChatHistory.tsx Extracts a memoized list item component and memoizes delete handler to reduce list re-renders.
.jules/bolt.md Documents a performance learning related to React list rendering and memoization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +68
<button
onClick={() => onSelect(session.id)}
className={cn(
"flex w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left transition-all duration-200 group",
isActive
? "text-xibe-text font-medium bg-xibe-surface-hover"
: "text-xibe-text-secondary hover:bg-xibe-surface-hover hover:text-xibe-text"
)}
>
<span className="flex-1 truncate text-[13px] leading-tight">{session.title}</span>

{/* ⚡ Bolt: Used CSS group-hover instead of React state to toggle delete button visibility to prevent O(N) re-renders of the entire list on hover */}
<span
onClick={(e) => onDelete(e, session.id)}
className="shrink-0 rounded p-1 text-xibe-text-dim/50 hover:text-xibe-error hover:bg-xibe-error/10 transition-colors animate-fade-in hidden group-hover:block"
title="Delete chat"
>
<Trash2 className="h-3.5 w-3.5" />
</span>

{/* ⚡ Bolt: Hide timestamp on hover using group-hover to make room for delete button without React state overhead */}
<span className="shrink-0 text-[10px] text-xibe-text-dim/40 tabular-nums block group-hover:hidden">{relativeTime(session.updated)}</span>
</button>
Comment on lines +82 to +86
const handleDelete = useCallback(async (e: React.MouseEvent, id: string) => {
e.stopPropagation();
await xibe.session.delete(id);
refresh();
};
}, [refresh]);
Comment on lines +130 to +136
<ChatHistoryItem
key={s.id}
onClick={() => onSelectSession(s.id)}
className={cn(
"flex w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left transition-all duration-200 group",
activeSessionId === s.id
? "text-xibe-text font-medium bg-xibe-surface-hover"
: "text-xibe-text-secondary hover:bg-xibe-surface-hover hover:text-xibe-text"
)}
>
<span className="flex-1 truncate text-[13px] leading-tight">{s.title}</span>

{/* ⚡ Bolt: Used CSS group-hover instead of React state to toggle delete button visibility to prevent O(N) re-renders of the entire list on hover */}
<span
onClick={(e) => handleDelete(e, s.id)}
className="shrink-0 rounded p-1 text-xibe-text-dim/50 hover:text-xibe-error hover:bg-xibe-error/10 transition-colors animate-fade-in hidden group-hover:block"
title="Delete chat"
>
<Trash2 className="h-3.5 w-3.5" />
</span>

{/* ⚡ Bolt: Hide timestamp on hover using group-hover to make room for delete button without React state overhead */}
<span className="shrink-0 text-[10px] text-xibe-text-dim/40 tabular-nums block group-hover:hidden">{relativeTime(s.updated)}</span>
</button>
session={s}
isActive={activeSessionId === s.id}
onSelect={onSelectSession}
onDelete={handleDelete}
/>
Comment thread .jules/bolt.md
Comment on lines +70 to +72
## 2023-10-27 - O(N) Re-renders in List Components
**Learning:** React list components mapped from arrays (e.g. `sessions.map(...)`) will experience severe O(N) re-renders when a single element changes state if the evaluation is done inline inside the map without memoization. Passing the active check (e.g., `isActive={activeSessionId === s.id}`) down to a `React.memo` wrapper component is necessary to convert this into an O(1) re-render (only the two affected components will update).
**Action:** When creating high-frequency updating list elements with individual active states in React, extract the items into `React.memo` components and pass simple boolean flags to prevent massive VDOM updates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants