Skip to content

Commit 28ff5ee

Browse files
⚡ Bolt: Memoize filtered models arrays in Chatbot
💡 What: Extracted array filtering logic for `hordeModels` and `otherModels` out of the render logic and wrapped it in `useMemo` in `client/components/apps/Chatbot.tsx`. 🎯 Why: Prevents expensive O(N) recalculations on every keystroke render (e.g. while streaming AI responses or when user is typing) for the large list of AI models. 📊 Impact: Considerably reduces computation on each render cycle by keeping derived state calculations limited to when dependencies (the model list) actually update. 🔬 Measurement: Verify by typing in the chat and monitoring the React Profiler for reduced computation in the `Chatbot` component render phase. Co-authored-by: Oxygen-Low <95589118+Oxygen-Low@users.noreply.github.com>
1 parent c99b62f commit 28ff5ee

3 files changed

Lines changed: 35 additions & 21 deletions

File tree

.jules/bolt.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
**Learning:** Static arrays that contain JSX elements (e.g. configuring categories or routing with icons) created inside a React component render function are re-allocated on every single render. This forces child components and `useMemo` hooks depending on them to constantly re-evaluate.
44
**Action:** Always define static configuration structures containing JSX elements outside the component body.
5+
56
## 2024-05-17 - [ReactMarkdown Memoization Pattern]
7+
68
**Learning:** In streaming chat interfaces (like `Chatbot.tsx`), placing inline object or function definitions (like the `components` prop for `ReactMarkdown`) inside a `React.memo` wrapped child component causes that heavy component to re-render constantly. This happens because the parent `ChatMessage` is memoized and might try to avoid re-renders, but when it does re-render due to prop changes (like new tokens in `m.content`), it recreates the `components` object, which then forces `ReactMarkdown` (a very heavy component with SyntaxHighlighter) to completely re-render from scratch instead of just updating text.
79
**Action:** Always extract static configuration objects, such as `ReactMarkdown`'s `components` mappings or custom renderers, outside of the React render body. If they depend on props/state, use `useMemo`. This prevents O(n) re-renders during frequent streaming state updates.

.jules/palette.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## 2023-10-27 - [Add aria-pressed to custom toggles]
2+
23
**Learning:** Custom toggle buttons and selection items (like themes, fonts, or feature switches) constructed with `button` elements often lack implicit state for screen readers. Using CSS classes alone for visual active state is insufficient for accessibility.
34
**Action:** Always map the boolean active state of a custom toggle or selection button to the `aria-pressed` attribute (e.g., `aria-pressed={isActive}`). For multi-selection or lists, `aria-selected` within a `role="listbox"` or `role="tablist"` might be more appropriate, but `aria-pressed` is crucial for standalone toggles.
5+
46
## 2025-02-28 - Add missing ARIA labels to Music Player
7+
58
**Learning:** Icon-only buttons without `aria-label` or `title` attributes are completely inaccessible to screen readers and difficult for sighted users to identify. When adding ARIA attributes to toggle buttons (like "Shuffle"), it is important to include `aria-pressed` to communicate the active state to assistive technologies.
69
**Action:** Always add `aria-label` and `title` to icon-only buttons. For stateful toggle buttons, implement `aria-pressed={state}` dynamically.

client/components/apps/Chatbot.tsx

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useRef } from "react";
1+
import React, { useState, useEffect, useRef, useMemo } from "react";
22
import {
33
Plus,
44
Trash2,
@@ -149,9 +149,7 @@ const ChatMessage = React.memo(
149149
: "bg-slate-900 border border-slate-800 text-slate-200",
150150
)}
151151
>
152-
<ReactMarkdown
153-
components={memoizedMarkdownComponents}
154-
>
152+
<ReactMarkdown components={memoizedMarkdownComponents}>
155153
{displayContent}
156154
</ReactMarkdown>
157155
</div>
@@ -270,6 +268,21 @@ export function ChatbotApp() {
270268
const lastParsedLengthRef = useRef(0);
271269
const messagesEndRef = useRef<HTMLDivElement>(null);
272270

271+
/**
272+
* ⚡ Bolt Performance Optimization:
273+
* Memoize filtered models (horde vs non-horde) so we don't recalculate
274+
* these arrays on every render (which happens frequently as the user types).
275+
*/
276+
const hordeModels = useMemo(() => {
277+
return models.filter((m) => m.provider === "horde");
278+
}, [models]);
279+
280+
const otherModels = useMemo(() => {
281+
return models.filter((m) => m.provider !== "horde");
282+
}, [models]);
283+
284+
const hasHordeModels = hordeModels.length > 0;
285+
273286
const fetchData = async () => {
274287
if (!session?.user?.id) return;
275288

@@ -702,31 +715,27 @@ export function ChatbotApp() {
702715
setSelection(m, p);
703716
}}
704717
>
705-
{models.some((m) => m.provider === "horde") && (
718+
{hasHordeModels && (
706719
<optgroup label="Default (AI Horde)">
707-
{models
708-
.filter((m) => m.provider === "horde")
709-
.map((m) => (
710-
<option
711-
key={`${m.provider}:${m.model_id}`}
712-
value={`${m.provider}:${m.model_id}`}
713-
>
714-
{formatModelLabel(m.provider, m.model_id)}
715-
</option>
716-
))}
717-
</optgroup>
718-
)}
719-
<optgroup label="Other Models">
720-
{models
721-
.filter((m) => m.provider !== "horde")
722-
.map((m) => (
720+
{hordeModels.map((m) => (
723721
<option
724722
key={`${m.provider}:${m.model_id}`}
725723
value={`${m.provider}:${m.model_id}`}
726724
>
727725
{formatModelLabel(m.provider, m.model_id)}
728726
</option>
729727
))}
728+
</optgroup>
729+
)}
730+
<optgroup label="Other Models">
731+
{otherModels.map((m) => (
732+
<option
733+
key={`${m.provider}:${m.model_id}`}
734+
value={`${m.provider}:${m.model_id}`}
735+
>
736+
{formatModelLabel(m.provider, m.model_id)}
737+
</option>
738+
))}
730739
</optgroup>
731740
</select>
732741
</div>

0 commit comments

Comments
 (0)