Skip to content

Commit 57077a8

Browse files
committed
perf: optimize BubbleList to avoid rerender
1 parent b01618b commit 57077a8

File tree

8 files changed

+50
-30
lines changed

8 files changed

+50
-30
lines changed

.changes/bubble-perf.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@matechat/react": patch:perf
3+
---
4+
5+
Optimize performance of `BubbleList` component, avoid extra rerender overheads.

playground/src/Chat.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MessageSquarePlus } from "lucide-react";
2-
import { useState } from "react";
2+
import { useCallback, useState } from "react";
33
import { BubbleList } from "../../dist/bubble";
44
import { Button } from "../../dist/button";
55
import { FileUpload } from "../../dist/file-upload";
@@ -17,6 +17,7 @@ import { useMateChat } from "../../dist/utils/core";
1717
export function Chat() {
1818
const initialMessages: MessageParam[] = [
1919
{
20+
id: "1",
2021
role: "user",
2122
content: "Hello, how are you?",
2223
avatar: {
@@ -25,6 +26,7 @@ export function Chat() {
2526
align: "right",
2627
},
2728
{
29+
id: "2",
2830
role: "assistant",
2931
content:
3032
"I'm doing well, thank you! How can I assist you today? \
@@ -48,10 +50,10 @@ export function Chat() {
4850
initialMessages,
4951
);
5052

51-
const onClear = () => {
53+
const onClear = useCallback(() => {
5254
setPrompt("");
5355
setMessages([]);
54-
};
56+
}, [setMessages]);
5557

5658
return (
5759
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">

src/bubble.tsx

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,7 @@ export function BubbleList({
252252
background = "right-solid",
253253
footer,
254254
pending,
255-
assistant = {
256-
avatar: {
257-
text: "A",
258-
},
259-
align: "left",
260-
},
255+
assistant,
261256
isPending = true,
262257
messages,
263258
threshold = 8,
@@ -324,6 +319,23 @@ export function BubbleList({
324319
}
325320
}, [isScrollAtBottom]);
326321

322+
const handleTouchStart = useCallback(() => {
323+
pauseScroll.current = true;
324+
}, []);
325+
326+
const handleTouchEnd = useCallback(() => {
327+
if (isScrollAtBottom()) {
328+
pauseScroll.current = false;
329+
scrollContainer(false);
330+
} else {
331+
pauseScroll.current = true;
332+
}
333+
}, [isScrollAtBottom, scrollContainer]);
334+
335+
const handleTouchMove = useCallback(() => {
336+
pauseScroll.current = true;
337+
}, []);
338+
327339
return (
328340
<div
329341
data-slot="bubble-list"
@@ -332,30 +344,19 @@ export function BubbleList({
332344
)}
333345
ref={containerRef}
334346
onWheel={handleWheel}
335-
onTouchStart={() => {
336-
pauseScroll.current = true;
337-
}}
338-
onTouchEnd={() => {
339-
if (isScrollAtBottom()) {
340-
pauseScroll.current = false;
341-
scrollContainer(false);
342-
} else {
343-
pauseScroll.current = true;
344-
}
345-
}}
346-
onTouchMove={() => {
347-
pauseScroll.current = true;
348-
}}
347+
onTouchStart={handleTouchStart}
348+
onTouchEnd={handleTouchEnd}
349+
onTouchMove={handleTouchMove}
349350
{...props}
350351
>
351352
<div
352353
data-slot="bubble-items"
353354
className="flex flex-col max-w-full flex-1 gap-4"
354355
ref={contentRef}
355356
>
356-
{messages.map((message, index) => (
357+
{messages.map((message) => (
357358
<div
358-
key={message.content.slice(0, 8) + index.toString()}
359+
key={message.id}
359360
data-slot="bubble-item"
360361
className={twMerge(
361362
clsx(

src/sender.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export interface SenderProps extends React.ComponentProps<"div"> {
106106
onSend?: (controller: AbortController) => void;
107107
toolbar?: React.ReactNode;
108108
}
109+
109110
export function Sender({
110111
className,
111112
initialMessage = "",

src/utils/backend.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createNanoEvents, type Emitter } from "nanoevents";
22
import OpenAI, { type ClientOptions } from "openai";
33
import type { Backend, Events, EventTypes, MessageParam } from "./types";
4+
import { getRandomId } from "./utils";
45

56
/**
67
* Configuration options for the `OpenAIBackend` class.
@@ -84,7 +85,7 @@ export class OpenAIBackend implements Backend {
8485
*/
8586
async input(prompt: string, options?: InputOptions): Promise<void> {
8687
this.emitter.emit("input", {
87-
id: Symbol(),
88+
id: getRandomId(16),
8889
type: "input",
8990
payload: { prompt },
9091
});
@@ -112,7 +113,7 @@ export class OpenAIBackend implements Backend {
112113
for await (const chunk of response) {
113114
const chunkMessage = chunk.choices[0].delta.content || "";
114115
this.emitter.emit("chunk", {
115-
id: Symbol(chunk.id),
116+
id: chunk.id,
116117
type: "chunk",
117118
payload: { chunk: chunkMessage },
118119
});
@@ -121,13 +122,13 @@ export class OpenAIBackend implements Backend {
121122
}
122123
} catch (error) {
123124
this.emitter.emit("error", {
124-
id: Symbol(),
125+
id: getRandomId(16),
125126
type: "error",
126127
payload: { error: (error as Error).message },
127128
});
128129
}
129130
this.emitter.emit("finish", {
130-
id: Symbol(),
131+
id: getRandomId(16),
131132
type: "finish",
132133
payload: { message: prompt },
133134
});

src/utils/chat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function useChat(
4242
setMessages((prevMessages) => [
4343
...prevMessages,
4444
{
45+
id: event.id,
4546
role: "user",
4647
name: "User",
4748
content: event.payload.prompt,
@@ -61,6 +62,7 @@ export function useChat(
6162
setMessages((prevMessages) => [
6263
...prevMessages,
6364
{
65+
id: event.id,
6466
role: "system",
6567
name: "Error",
6668
content: event.payload.error,
@@ -87,6 +89,7 @@ export function useChat(
8789
return [
8890
...prev,
8991
{
92+
id: event.id,
9093
role: "assistant",
9194
content: event.payload.chunk,
9295
avatar: {

src/utils/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Emitter } from "nanoevents";
22
import type { AvatarProps } from "../bubble";
33

44
export interface BaseEvent<T extends string> {
5-
id: symbol;
5+
id: string;
66
type: T;
77
payload?: unknown;
88
timestamp?: number;
@@ -47,6 +47,7 @@ export type Events = {
4747
};
4848

4949
export interface MessageParam {
50+
id: string;
5051
role: "user" | "assistant" | "system" | "developer";
5152
name?: string;
5253
content: string;

src/utils/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function getRandomId(length: number) {
2+
if (typeof window === "undefined") {
3+
return Math.random().toString(36).substring(2, length);
4+
}
5+
return window.crypto.randomUUID();
6+
}

0 commit comments

Comments
 (0)