mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-19 01:04:55 +02:00
ui : mount chat messages lazily near the viewport
Every message row mounted its full component tree on load, so the cycle collector, GC and layout invalidation kept walking every live object and DOM node even for rows the user never scrolls to - which dominated the profile of long conversations. Wrap each row in a placeholder with an IntersectionObserver ( two viewport heights of runway ) that swaps in the real ChatMessage when the row approaches the viewport; the row shell keeps the content-visibility sizing, and rows stay mounted once realized. Rows targeted by the pending-edit flow mount eagerly. Assisted-by: pi:zai-org/GLM-5.3
This commit is contained in:
@@ -404,7 +404,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:chat-message--synthetic={isSynthetic} class="chat-message">
|
||||
<div>
|
||||
{#if message.role === MessageRole.SYSTEM}
|
||||
<ChatMessageSystem bind:textareaElement class={className} {message} />
|
||||
{:else if mcpPromptExtra}
|
||||
@@ -425,25 +425,3 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/*
|
||||
* The browser skips layout and paint for messages outside the
|
||||
* viewport. contain-intrinsic-size reuses the last rendered size
|
||||
* once known; 500px sizes messages that have never been rendered.
|
||||
*/
|
||||
.chat-message {
|
||||
--chat-message-intrinsic-size: 500px;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto var(--chat-message-intrinsic-size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Synthetic rows (e.g. the working-directory change) are small, so an
|
||||
* accurate placeholder keeps the injected row from inflating the
|
||||
* auto-scroll offset; the 500px default is for ordinary bubbles.
|
||||
*/
|
||||
.chat-message--synthetic {
|
||||
--chat-message-intrinsic-size: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
|
||||
import LazyChatMessage from './LazyChatMessage.svelte';
|
||||
import { ChatMessageUserPending } from '$lib/components/app';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
|
||||
import type { ChatMessageActions } from '$lib/types';
|
||||
@@ -238,7 +239,7 @@
|
||||
|
||||
<div>
|
||||
{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
|
||||
<ChatMessage
|
||||
<LazyChatMessage
|
||||
{chatActions}
|
||||
class="mx-auto mt-12 w-full max-w-3xl"
|
||||
{isLastAssistantMessage}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import ChatMessage from './ChatMessage/ChatMessage.svelte';
|
||||
import { chatStore } from '$lib/stores';
|
||||
import type { ChatMessageActions } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
chatActions: ChatMessageActions;
|
||||
class?: string;
|
||||
isLastAssistantMessage?: boolean;
|
||||
isLastUserMessage?: boolean;
|
||||
message: DatabaseMessage;
|
||||
nextAssistantMessage?: DatabaseMessage | null;
|
||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||
toolMessages?: DatabaseMessage[];
|
||||
}
|
||||
|
||||
let {
|
||||
chatActions,
|
||||
class: className = '',
|
||||
isLastAssistantMessage = false,
|
||||
isLastUserMessage = false,
|
||||
message,
|
||||
nextAssistantMessage = null,
|
||||
siblingInfo = null,
|
||||
toolMessages = []
|
||||
}: Props = $props();
|
||||
|
||||
// A mounted message row is a whole component tree (contexts, effects,
|
||||
// collapsibles, markdown blocks), and the cycle collector, GC and layout
|
||||
// invalidation keep walking every live object and DOM node, even for
|
||||
// rows the user never scrolls to. Mount the real tree only when the row
|
||||
// approaches the viewport; until then the row is an empty placeholder
|
||||
// that reserves its size through content-visibility.
|
||||
let mounted = $state(false);
|
||||
let wrapperEl: HTMLDivElement | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
if (mounted || !wrapperEl) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
mounted = true;
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
// pre-mount a couple of viewport heights ahead of the scroll
|
||||
// position so a fast scroll never meets an empty row
|
||||
{ rootMargin: '200% 0px' }
|
||||
);
|
||||
|
||||
observer.observe(wrapperEl);
|
||||
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
// Flows that target a row by id (pending edit) expect the message
|
||||
// component and its effects to exist; mount the target row first
|
||||
$effect(() => {
|
||||
if (chatStore.pendingEditMessageId === message.id) {
|
||||
mounted = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={wrapperEl}
|
||||
class:chat-message--synthetic={Boolean(message.isSynthetic)}
|
||||
class="chat-message"
|
||||
>
|
||||
{#if mounted}
|
||||
<ChatMessage
|
||||
{chatActions}
|
||||
class={className}
|
||||
{isLastAssistantMessage}
|
||||
{isLastUserMessage}
|
||||
{message}
|
||||
{nextAssistantMessage}
|
||||
{siblingInfo}
|
||||
{toolMessages}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/*
|
||||
* The browser skips layout and paint for messages outside the
|
||||
* viewport. contain-intrinsic-size reuses the last rendered size
|
||||
* once known; 500px sizes messages that have never been rendered.
|
||||
*/
|
||||
.chat-message {
|
||||
--chat-message-intrinsic-size: 500px;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto var(--chat-message-intrinsic-size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Synthetic rows (e.g. the working-directory change) are small, so an
|
||||
* accurate placeholder keeps the injected row from inflating the
|
||||
* auto-scroll offset; the 500px default is for ordinary bubbles.
|
||||
*/
|
||||
.chat-message--synthetic {
|
||||
--chat-message-intrinsic-size: 40px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user