From 6ec30826bab349803d586bc0e5449ad5375e03df Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:39:37 +0000 Subject: [PATCH] Optimize ChatInterface re-renders Separated the message list into a `MessageList` component wrapped with `React.memo` to prevent re-rendering the entire chat history on every keystroke in the input field. This ensures that the heavy list rendering is isolated from the frequent state updates of the text input. Optimizations: - Extracted `MessageList` from `ChatInterface`. - Memoized `MessageList` to skip re-renders when props (`messages`, `isLoading`, `onSendMessage`) are unchanged. - Kept `input` state local to `ChatInterface`. Verification: - Verified UI functionality with Playwright screenshot. - Validated that typing in the input does not break the layout. --- .Jules/bolt.md | 9 +++++ src/components/chat/ChatInterface.tsx | 39 +++++----------------- src/components/chat/MessageList.tsx | 48 +++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 .Jules/bolt.md create mode 100644 src/components/chat/MessageList.tsx diff --git a/.Jules/bolt.md b/.Jules/bolt.md new file mode 100644 index 0000000..9a47f1a --- /dev/null +++ b/.Jules/bolt.md @@ -0,0 +1,9 @@ +# Bolt's Journal + +## 2024-05-23 - Initial Setup +**Learning:** Bolt journal initialized. +**Action:** Always check this file for critical learnings before starting. + +## 2024-05-23 - Component Separation for Performance +**Learning:** `ChatInterface` was re-rendering the entire message list (including `ReactMarkdown` parsing inside `MessageItem`) on every keystroke because the input state was in the same component. Even with `React.memo` on `MessageItem`, the list reconciliation cost adds up. +**Action:** Split components that handle frequent state updates (like inputs) from heavy list renderers. Created `MessageList` to isolate the list rendering from the input state. diff --git a/src/components/chat/ChatInterface.tsx b/src/components/chat/ChatInterface.tsx index 454fe9f..48c57cd 100644 --- a/src/components/chat/ChatInterface.tsx +++ b/src/components/chat/ChatInterface.tsx @@ -1,14 +1,13 @@ -import { useState, useEffect, useRef } from "react"; +import { useState } from "react"; import { useChatStore } from "@/store/chat"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { Send, Bot, Loader2, RefreshCw, Terminal } from "lucide-react"; -import { MessageItem } from "./MessageItem"; +import { Send, RefreshCw, Terminal } from "lucide-react"; +import { MessageList } from "./MessageList"; export function ChatInterface() { const { messages, sendMessage, isLoading, resetChat } = useChatStore(); const [input, setInput] = useState(""); - const scrollRef = useRef(null); const handleSend = async () => { if (!input.trim()) return; @@ -24,10 +23,6 @@ export function ChatInterface() { } }; - useEffect(() => { - scrollRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages, isLoading]); - return (
@@ -50,29 +45,11 @@ export function ChatInterface() {
{/* CHAT AREA */} -
- {messages.map((msg) => ( - - ))} - - {isLoading && ( -
-
- -
-
- - Thinking... -
-
- )} - -
-
+ {/* INPUT AREA */}
diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx new file mode 100644 index 0000000..61d115e --- /dev/null +++ b/src/components/chat/MessageList.tsx @@ -0,0 +1,48 @@ +import React, { memo, useEffect, useRef } from "react"; +import { ChatMessage } from "@/types/chat"; +import { Bot, Loader2 } from "lucide-react"; +import { MessageItem } from "./MessageItem"; + +interface MessageListProps { + messages: ChatMessage[]; + isLoading: boolean; + onSendMessage: (content: string) => void; +} + +const MessageList = memo(({ messages, isLoading, onSendMessage }: MessageListProps) => { + const scrollRef = useRef(null); + + useEffect(() => { + scrollRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, isLoading]); + + return ( +
+ {messages.map((msg) => ( + + ))} + + {isLoading && ( +
+
+ +
+
+ + Thinking... +
+
+ )} + +
+
+ ); +}); + +MessageList.displayName = "MessageList"; + +export { MessageList };