Skip to content

⚡ Bolt: [Performance] Eliminate O(N) re-renders in ChatHistory via CSS group-hover & memoization#69

Merged
iotserver24 merged 1 commit intomainfrom
bolt-performance-chathistory-14370465157369467408
May 3, 2026
Merged

⚡ Bolt: [Performance] Eliminate O(N) re-renders in ChatHistory via CSS group-hover & memoization#69
iotserver24 merged 1 commit intomainfrom
bolt-performance-chathistory-14370465157369467408

Conversation

@iotserver24
Copy link
Copy Markdown
Owner

@iotserver24 iotserver24 commented May 3, 2026

💡 What

Refactored the ChatHistory React component to drop the use of hoveredId via React useState. The interactive visibility of timestamps vs delete buttons is now exclusively managed via Tailwind CSS's native group-hover utility classes. The list component itself was also wrapped in React.memo().

🎯 Why

Prop-drilling or managing local interactive state (like hoveredId on mouseenter/mouseleave) within a list component mapping an array of items creates O(N) re-renders when interacting with any single item. By deferring visual changes on hover to native CSS rules, we entirely sidestep React's rendering lifecycle for this interaction.

📊 Impact

Reduces list re-renders to O(1) on mount, entirely eliminating the O(N) overhead during user hover interactions or when the parent App is updating streaming state.

🔬 Measurement

Run pnpm test and pnpm lint to verify code validity. Visual verifications via Playwright assert the frontend styling remains completely unchanged.


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

Summary by CodeRabbit

  • Performance

    • Improved chat history interactions with optimized hover effects and component memoization.
  • Bug Fixes

    • Enhanced error handling for message content parsing.
  • Documentation

    • Added best practices guide for optimizing list component rendering performance.

…S group-hover & memoization

- Refactored `ChatHistory` list to use Tailwind CSS `group-hover` utility classes instead of React state for hover transitions, eliminating O(N) re-renders on hover interactions.
- Wrapped `ChatHistory` component with `React.memo` to prevent cascading updates from frequent parent re-renders.
- Fixed a type error in `ext/src/providers/chat-view-provider.ts` where marked `.parse` returning a union was incorrectly called with `.catch()`.
@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 3, 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 3, 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: 2fc531f8-916a-45ff-8d62-ba6ee1c6e10f

📥 Commits

Reviewing files that changed from the base of the PR and between 5703591 and 2ed36c2.

📒 Files selected for processing (3)
  • .jules/bolt.md
  • packages/desktop/src/renderer/components/ChatHistory.tsx
  • packages/ext/src/providers/chat-view-provider.ts

📝 Walkthrough

Walkthrough

This PR addresses O(N) re-render cascades in chat lists by documenting the pattern, wrapping ChatHistory with memo, replacing React state-driven hover with CSS group-hover classes, and improving async message parsing robustness in getParsedHistory().

Changes

Chat List Performance Optimization

Layer / File(s) Summary
Documentation & Pattern
.jules/bolt.md
New guidance documents O(N) cascade re-renders caused by per-item React state (e.g., hoveredId) in lists and prescribes CSS/Tailwind hover pseudo-classes combined with React.memo as the solution.
Component Memoization & Hover Refactor
packages/desktop/src/renderer/components/ChatHistory.tsx
ChatHistory wrapped with memo. React hoveredId state removed. Delete button and timestamp now render always-present but toggle visibility via hidden group-hover:block and block group-hover:hidden respectively, eliminating conditional rendering.
Async Parsing Robustness
packages/ext/src/providers/chat-view-provider.ts
getParsedHistory() refactored from single .catch() to per-message try/catch. Conditionally awaits marked.parse() result if it returns a Promise, falling back to original content on parse failure.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 Hovering lists no more cascade,
CSS now guides the shade,
Memo wraps the render chain,
O(N) sorrows now abstain!
Parse with grace, no promise frown,
Chat history speeds on down. 🚀

✨ 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-performance-chathistory-14370465157369467408

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

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

@iotserver24 iotserver24 marked this pull request as ready for review May 3, 2026 17:28
Copilot AI review requested due to automatic review settings May 3, 2026 17:28
@iotserver24 iotserver24 merged commit c94d229 into main May 3, 2026
5 of 6 checks passed
@iotserver24 iotserver24 deleted the bolt-performance-chathistory-14370465157369467408 branch May 3, 2026 17:28
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

Refactors UI hover behavior in the Desktop ChatHistory session list to reduce render churn by moving hover-driven UI toggles from React state to CSS (group-hover) and memoizing the component; also hardens VS Code extension history HTML parsing around marked.parse() return types.

Changes:

  • Replaced hoveredId React state with Tailwind group-hover to toggle timestamp vs delete affordance in ChatHistory.
  • Wrapped ChatHistory in React.memo() to avoid unnecessary re-renders when props are stable.
  • Updated extension getParsedHistory() to safely handle both sync/async marked.parse() results.
  • Documented the performance learning in .jules/bolt.md.

Reviewed changes

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

File Description
packages/ext/src/providers/chat-view-provider.ts Makes history markdown parsing robust when marked.parse() is sync or async.
packages/desktop/src/renderer/components/ChatHistory.tsx Removes hover state from list rendering and memoizes the component to reduce re-renders.
.jules/bolt.md Records the “CSS hover over React hover state in lists” performance guideline.

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

Comment on lines +106 to +112
<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>
Comment on lines +148 to +149
const parsed = marked.parse(m.content);
html = parsed instanceof Promise ? await parsed : parsed;
@iotserver24
Copy link
Copy Markdown
Owner Author

Xibe review this

@xibe-review
Copy link
Copy Markdown

xibe-review Bot commented May 3, 2026

Hey @iotserver24! 👋

I'll go through the changes and help you out! 🔍

Starting the review now...

@xibe-review
Copy link
Copy Markdown

xibe-review Bot commented May 3, 2026

🔍 Final Review for @iotserver24

PR: ⚡ Bolt: [Performance] Eliminate O(N) re-renders in ChatHistory via CSS group-hover & memoization
User Request: “Xibe review this” (interpreted as a full code review covering security, quality, and correctness)


🔴 Security Concerns (Top Priority)

1. Cross-Site Scripting (XSS) via Unsanitized Markdown Output

File: packages/ext/src/providers/chat-view-provider.ts
Lines: 148–152

const parsed = marked.parse(m.content);
html = parsed instanceof Promise ? await parsed : parsed;

Why it’s critical:

  • marked does not sanitize HTML by default. If user-controlled m.content contains <script> tags, onerror attributes, or other dangerous HTML, it will be rendered as raw HTML in the VSCode webview.
  • Even with sandbox restrictions, stored XSS is possible (e.g., javascript: URLs in attributes, or breaking layout to phish credentials).
  • This is a stored XSS vector if chat history is persisted.

Fix:
Sanitize output with DOMPurify after parsing. Use a library compatible with your runtime:

import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';

const window = new JSDOM('').window;
const purify = DOMPurify(window);

try {
  const parsed = marked.parse(m.content);
  const rawHtml = await parsed;
  html = purify.sanitize(rawHtml);
} catch (e) {
  // fallback to escaped content as plain text?
  html = m.content;
}

2. Potential Stored XSS via Chat Description Title

File: packages/desktop/src/renderer/components/ChatHistory.tsx
Line: ~114 (rendering s.title)

<span>{s.title}</span>

Why it’s a concern:

  • React’s JSX automatically escapes string values, so this is safe if s.title is always a string.
  • However, if s.title can ever be an object (e.g., due to API inconsistency), React might render [object Object] or throw.
  • To harden against edge cases, cast to string:
<span>{String(s.title)}</span>

Additionally, ensure the backend sanitizes or rejects malicious content in session titles.


🔵 User-Specific Analysis: “Xibe review this”

Your PR focuses on performance – eliminating O(N) re-renders by moving hover state to CSS and using React.memo. This is a solid improvement and the core changes are well-implemented.

However, reviewing the whole PR reveals three categories of issues that need attention before merging:

Category Priority Issue
Security 🔴 XSS in chat-view-provider.ts (unsanitized marked.parse)
Accessibility 🟡 Replaced <button> with <span> for delete action
Robustness 🟡 Missing e.stopPropagation() on delete click triggers unintended navigation
Code Quality 🔵 Unused catch parameter, redundant Promise check, future date in docs

The performance gain is real, but the accessibility regression is a breaking change for keyboard/screen-reader users. The XSS vulnerability is a hard blocker.


🛠️ Recommended Changes (Actionable Fixes)

🔴 Critical: Add HTML Sanitization (chat-view-provider.ts)

Replace the current marked.parse block with sanitized output (see fix above).
Also update the lockfile to ensure marked is on a recent version (≥4.0.10 to avoid known vulnerabilities).

🟡 High Priority: Restore Keyboard-Accessible Delete Button (ChatHistory.tsx)

Lines 108–110 – Change <span> back to a <button>:

<button
  type="button"
  onClick={(e) => {
    e.stopPropagation();
    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 invisible group-hover:visible"
  title="Delete chat"
  aria-label={`Delete session ${s.title}`}
>
  <Trash2 className="h-3.5 w-3.5" />
</button>

Why the changes?

  • type="button" prevents accidental form submission.
  • stopPropagation prevents triggering the outer onSelectSession.
  • Use invisible/visible instead of hidden/block to avoid layout shift on hover.
  • Add aria-label for screen readers.

🟡 Medium Priority: Prevent Layout Shift (ChatHistory.tsx)

Apply the same invisible/visible toggle to the timestamp element (line ~113) to maintain stable width:

<span className="invisible group-hover:visible ml-auto text-xs text-xibe-text-dim/20 pointer-events-none">
  {formatRelativeTime(s.updatedAt)}
</span>

🔵 Low Priority: Code Cleanup

  • chat-view-provider.ts – Remove unused catch (e), change to catch { or catch (_e).
  • chat-view-provider.ts – Simplify instanceof Promise logic:
    html = await marked.parse(m.content);
  • bolt.md – Correct the future date (2026-05-03 → actual date) and consider merging duplicate entries.

📊 Comprehensive Analysis

✅ What’s Great

  • Performance improvement is real – CSS group-hover eliminates all JavaScript re-render overhead for hover interactions.
  • React.memo is correctly applied to the entire list, preventing unnecessary renders when sibling state changes.
  • Clean state removal – dropped hoveredId, mouseEnter/mouseLeave handlers, simplifying the component.
  • Good error handling in chat-view-provider.ts – the try/catch gracefully falls back to raw content.
  • Clear documentation in .jules/bolt.md – the pattern is well-explained and reusable.

⚠️ What Needs Work

Area Current State Impact Fix Priority
Security Unsanitized markdown → stored XSS Critical – potential script injection 🔴 Highest
Accessibility Delete button not keyboard-accessible Screen reader & keyboard users blocked 🟡 High
User Experience Layout shift on hover Jarring visual reflow 🟡 Medium
Prop drift risk Memoized parent list with non-primitive props May cause stale UI if props change but reference stays same 🔵 Low
Code clarity instanceof Promise and unused catch Minor lint noise 🔵 Low

🧪 Testing Recommendation

After applying fixes, re-run both tests and manual verification:

  1. Keyboard navigation – Tab to a chat item, then Tab to the delete button; press Enter/Space to delete. Verify no accidental navigation.
  2. Visual regression – Playwright screenshot comparison to ensure no layout shift on hover.
  3. Security test – Inject <img src=x onerror=alert(1)> as a chat message title and confirm no dialog appears.

📎 Summary

Metric Value
Files Reviewed 3
Issues Found 2 🔴 Critical, 3 🟡 Warnings, 2 🔵 Suggestions
Security Blocker Yes – XSS in chat-view-provider.ts
Recommendation ⚠️ Request changes – must fix security + accessibility before merge

Your performance refactor is clean and valuable. Once the XSS sanitization and accessibility regression are addressed, this PR will be a model of modern React optimization. Great work on the core logic – just need to button up the edges.


🤖 Powered by Xibe AI
📊 Analysis: 5595 characters analyzed across 3 files
💙 Real-time Analytics📚 Documentation

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