perf(chat): debounce persisted writes and re-render only the streaming message

- the Zustand persist middleware serialized every saved session and wrote to AsyncStorage on each streamed token (appendToMessage -> set); writes are now debounced (400 ms) and flushed when the app leaves the foreground
- every token re-rendered every MessageBubble, re-parsing the Markdown of the whole conversation; rows are memoized so only the changed message renders (covered by MessageList.test.tsx)
This commit is contained in:
evandro committed 2026-10-10 04:28:38 -03:00
1 parent 200fb6afd7
commit 0e5b38fd15
6 files changed
+252 -13

No files matched your search

+44 -10
View File
@@ -14,6 +14,41 @@ interface MessageListProps {
onRegenerate: () => void;
}
interface MessageRowProps {
message: ChatMessage;
delayed: boolean;
isStreaming: boolean;
isLastAssistant: boolean;
onEdit: (id: string) => void;
onDelete: (id: string) => void;
onRegenerate: () => void;
}
// Memoized so that a streamed token only re-renders (and re-parses the Markdown of)
// the message that changed, not the whole conversation.
const MessageRow = React.memo(function MessageRow({
message,
delayed,
isStreaming,
isLastAssistant,
onEdit,
onDelete,
onRegenerate,
}: MessageRowProps) {
return (
<Animated.View entering={FadeInDown.duration(300).delay(delayed ? 100 : 0)}>
<MessageBubble
message={message}
onEdit={() => onEdit(message.id)}
onRegenerate={onRegenerate}
onDelete={() => onDelete(message.id)}
isStreaming={isStreaming}
isLastAssistant={isLastAssistant}
/>
</Animated.View>
);
});
export function MessageList({ messages, isStreaming, onEdit, onDelete, onRegenerate }: MessageListProps) {
const flatListRef = useRef<FlatList>(null);
const lastAssistantId = findLastAssistantId(messages);
@@ -34,16 +69,15 @@ export function MessageList({ messages, isStreaming, onEdit, onDelete, onRegener
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<Animated.View entering={FadeInDown.duration(300).delay(index > messages.length - 4 ? 100 : 0)}>
<MessageBubble
message={item}
onEdit={() => onEdit(item.id)}
onRegenerate={onRegenerate}
onDelete={() => onDelete(item.id)}
isStreaming={isStreaming}
isLastAssistant={item.id === lastAssistantId}
/>
</Animated.View>
<MessageRow
message={item}
delayed={index > messages.length - 4}
isStreaming={isStreaming}
isLastAssistant={item.id === lastAssistantId}
onEdit={onEdit}
onDelete={onDelete}
onRegenerate={onRegenerate}
/>
)}
contentContainerStyle={styles.listContent}
/>