From 30ae5192266d66fbc7c72072b7ef5e456b5830da Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:09:43 +0100 Subject: [PATCH] perf: throttle message list rebuild to once per animation frame during streaming (#21885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Messages.svelte rebuilds the message list by walking the parent chain and creating spread copies on every history.messages change. During streaming, this runs on every token — hundreds of times per second — even though each ResponseMessage already has its own reactive binding for content updates. Throttle the rebuild to once per animation frame (~60Hz) during content-only updates, while keeping immediate rebuilds for structural changes (currentId changes like chat switches, navigation, or new messages). Adds onDestroy cleanup for the pending rAF. --- src/lib/components/chat/Messages.svelte | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 3c8d7485b..f410686c0 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -9,7 +9,7 @@ currentChatPage, temporaryChatEnabled } from '$lib/stores'; - import { tick, getContext, onMount, createEventDispatcher } from 'svelte'; + import { tick, getContext, onMount, onDestroy, createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); import { toast } from 'svelte-sonner'; @@ -73,7 +73,10 @@ messagesLoading = false; }; - $: if (history.currentId) { + let pendingRebuild = null; + let lastCurrentId = null; + + const buildMessages = () => { let _messages = []; let message = history.messages[history.currentId]; @@ -91,6 +94,28 @@ } messages = _messages; + }; + + // Throttle message list rebuilds to once per animation frame during streaming. + // Structural changes (currentId change) always rebuild immediately. + $: if (history.currentId) { + const currentIdChanged = history.currentId !== lastCurrentId; + lastCurrentId = history.currentId; + + if (currentIdChanged) { + // Structural change: new chat, navigation, new message — rebuild immediately + cancelAnimationFrame(pendingRebuild); + pendingRebuild = null; + buildMessages(); + } else if (history.messages) { + // Content update (streaming) — throttle to once per frame + if (!pendingRebuild) { + pendingRebuild = requestAnimationFrame(() => { + pendingRebuild = null; + buildMessages(); + }); + } + } } else { messages = []; } @@ -395,6 +420,10 @@ showMessage({ id: parentMessageId }, false); }; + onDestroy(() => { + cancelAnimationFrame(pendingRebuild); + }); + const triggerScroll = () => { if (autoScroll) { const element = document.getElementById('messages-container');