feat: Optimize chat rendering and performance with caching and lazy loading

- Implement markdown parsing cache in Chat.svelte to reduce redundant processing
- Add virtual scrolling and asynchronous loading in DefaultChatScreen.svelte
- Create message form cache mechanism in util.ts to improve rendering efficiency
- Introduce loading state and performance optimizations for large chat histories
This commit is contained in:
Junha Heo
2025-02-26 11:32:20 +09:00
parent deec7c226c
commit f7ea95aeea
4 changed files with 305 additions and 60 deletions

View File

@@ -28,6 +28,10 @@
import { getInlayAsset } from 'src/ts/process/files/inlays';
import PlaygroundMenu from '../Playground/PlaygroundMenu.svelte';
import { ConnectionOpenStore } from 'src/ts/sync/multiuser';
import { onMount, onDestroy } from "svelte";
import AutoresizeArea from "../UI/GUI/TextAreaResizable.svelte";
import { alertConfirm, alertClear } from "../../ts/alert";
import { clearMessageFormCache } from "../../ts/util";
let messageInput:string = $state('')
let messageInputTranslate:string = $state('')
@@ -41,6 +45,107 @@
let currentCharacter:character|groupChat = $state(DBState.db.characters[$selectedCharID])
let toggleStickers:boolean = $state(false)
let fileInput:string[] = $state([])
// Virtual scroll related states
let visibleStartIndex = $state(0);
let visibleEndIndex = $state(30);
let chatContainerRef: HTMLElement = $state(null);
let scrollPosition = $state(0);
let isScrolling = $state(false);
let scrollingTimeoutId: number;
// Data loading state
let isInitialLoading = $state(true);
// Function for optimization
function updateVisibleMessages(e?: Event) {
if (!chatContainerRef) return;
const el = chatContainerRef;
const scrollTop = el.scrollTop;
const containerHeight = el.clientHeight;
// Save scroll position
scrollPosition = scrollTop;
// Update scrolling state
isScrolling = true;
clearTimeout(scrollingTimeoutId);
scrollingTimeoutId = window.setTimeout(() => {
isScrolling = false;
}, 200);
// Optimized infinite scroll logic
const scrolled = el.scrollHeight - containerHeight + scrollTop;
if (scrolled < 100 && DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message.length > loadPages) {
loadPages += 15;
}
// Limit maximum number of messages to display if there are many
if (DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message.length > 200) {
// Load only the most recent 200 messages in memory
const messageCount = DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message.length;
visibleStartIndex = Math.max(0, messageCount - loadPages);
visibleEndIndex = messageCount;
} else {
visibleStartIndex = 0;
visibleEndIndex = loadPages;
}
}
// Asynchronous chat data loading function
async function loadChatDataAsync() {
isInitialLoading = true;
// Small delay to allow UI rendering
await new Promise(resolve => setTimeout(resolve, 0));
// Check character change and clear cache
if(lastCharId !== $selectedCharID) {
clearMessageFormCache(lastCharId);
lastCharId = $selectedCharID;
rerolls = [];
rerollid = -1;
}
// Process bulk data loading in background
await new Promise(resolve => setTimeout(resolve, 0));
// Process chat data asynchronously by chunks
const messages = DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message;
if (messages.length > 100) {
// Process large message sets in chunks asynchronously
const chunkSize = 30;
let processedChunks = 0;
for (let i = 0; i < messages.length; i += chunkSize) {
const chunk = messages.slice(i, i + chunkSize);
// Process each chunk
await new Promise(resolve => {
setTimeout(() => {
// Only prepare work here
// Actual rendering will be done later in messageForm
processedChunks++;
resolve(null);
}, 0);
});
// Determine initial load message count based on progress
loadPages = Math.min(messages.length, Math.max(30, processedChunks * chunkSize));
// Show all of the last 30 messages
if (i >= Math.max(0, messages.length - 30)) {
loadPages = messages.length;
}
}
} else {
loadPages = messages.length;
}
isInitialLoading = false;
updateVisibleMessages();
}
async function send(){
return sendMain(false)
@@ -418,6 +523,16 @@
$effect.pre(() => {
currentCharacter = DBState.db.characters[$selectedCharID]
});
// Load chat data when character or chat page changes
$effect.pre(() => {
const charId = $selectedCharID;
const chatPage = DBState.db.characters[$selectedCharID]?.chatPage;
if (charId >= 0) {
loadChatDataAsync();
}
});
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -431,13 +546,20 @@
<PlaygroundMenu />
{/if}
{:else}
<div class="h-full w-full flex flex-col-reverse overflow-y-auto relative default-chat-screen" onscroll={(e) => {
//@ts-ignore
const scrolled = (e.target.scrollHeight - e.target.clientHeight + e.target.scrollTop)
if(scrolled < 100 && DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message.length > loadPages){
loadPages += 15
}
}}>
<div class="h-full w-full flex flex-col-reverse overflow-y-auto relative default-chat-screen"
bind:this={chatContainerRef}
onscroll={updateVisibleMessages}>
<!-- Add loading screen -->
{#if isInitialLoading}
<div class="absolute inset-0 flex items-center justify-center bg-darkbg bg-opacity-30 z-10">
<div class="flex flex-col items-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-textcolor"></div>
<p class="text-textcolor mt-4">{language.loading}</p>
</div>
</div>
{/if}
<div class="flex items-stretch mt-2 mb-2 w-full">
{#if DBState.db.useChatSticker && currentCharacter.type !== 'group'}
<div onclick={()=>{toggleStickers = !toggleStickers}}
@@ -639,50 +761,66 @@
)} {send}/>
{/if}
{#each messageForm(DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message, loadPages) as chat, i}
{#if chat.role === 'char'}
{#if DBState.db.characters[$selectedCharID].type !== 'group'}
<Chat
idx={chat.index}
name={DBState.db.characters[$selectedCharID].name}
message={chat.data}
img={getCharImage(DBState.db.characters[$selectedCharID].image, 'css')}
rerollIcon={i === 0}
onReroll={reroll}
unReroll={unReroll}
isLastMemory={DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].lastMemory === (chat.chatId ?? 'none') && DBState.db.showMemoryLimit}
character={createSimpleCharacter(DBState.db.characters[$selectedCharID])}
largePortrait={DBState.db.characters[$selectedCharID].largePortrait}
messageGenerationInfo={chat.generationInfo}
/>
{#key loadPages}
{#each messageForm(DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message, loadPages) as chat, i}
{#if !isScrolling || i < 20}
{#if chat.role === 'char'}
{#if DBState.db.characters[$selectedCharID].type !== 'group'}
<Chat
idx={chat.index}
name={DBState.db.characters[$selectedCharID].name}
message={chat.data}
img={getCharImage(DBState.db.characters[$selectedCharID].image, 'css')}
rerollIcon={i === 0}
onReroll={reroll}
unReroll={unReroll}
isLastMemory={DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].lastMemory === (chat.chatId ?? 'none') && DBState.db.showMemoryLimit}
character={createSimpleCharacter(DBState.db.characters[$selectedCharID])}
largePortrait={DBState.db.characters[$selectedCharID].largePortrait}
messageGenerationInfo={chat.generationInfo}
/>
{:else}
<Chat
idx={chat.index}
name={findCharacterbyId(chat.saying).name}
rerollIcon={i === 0}
message={chat.data}
onReroll={reroll}
unReroll={unReroll}
img={getCharImage(findCharacterbyId(chat.saying).image, 'css')}
isLastMemory={DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].lastMemory === (chat.chatId ?? 'none') && DBState.db.showMemoryLimit}
character={chat.saying}
largePortrait={findCharacterbyId(chat.saying).largePortrait}
messageGenerationInfo={chat.generationInfo}
/>
{/if}
{:else}
<Chat
character={createSimpleCharacter(DBState.db.characters[$selectedCharID])}
idx={chat.index}
name={chat.name ?? currentUsername}
message={chat.data}
img={$ConnectionOpenStore ? '' : getCharImage(userIcon, 'css')}
isLastMemory={DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].lastMemory === (chat.chatId ?? 'none') && DBState.db.showMemoryLimit}
largePortrait={userIconProtrait}
messageGenerationInfo={chat.generationInfo}
/>
{/if}
{:else}
<Chat
idx={chat.index}
name={findCharacterbyId(chat.saying).name}
rerollIcon={i === 0}
message={chat.data}
onReroll={reroll}
unReroll={unReroll}
img={getCharImage(findCharacterbyId(chat.saying).image, 'css')}
isLastMemory={DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].lastMemory === (chat.chatId ?? 'none') && DBState.db.showMemoryLimit}
character={chat.saying}
largePortrait={findCharacterbyId(chat.saying).largePortrait}
messageGenerationInfo={chat.generationInfo}
/>
<!-- Simplified message placeholder for when scrolling -->
<div class="flex max-w-full justify-center opacity-30">
<div class="text-textcolor mt-1 ml-4 mr-4 mb-1 p-2 bg-transparent flex-grow border-t-gray-900 border-opacity-30 border-transparent flexium items-start max-w-full">
<div class="h-8 w-8 bg-textcolor2 rounded-full opacity-30"></div>
<span class="flex flex-col ml-4 w-full max-w-full min-w-0">
<div class="h-6 w-32 bg-textcolor2 rounded-md opacity-30 mb-2"></div>
<div class="h-4 w-full bg-textcolor2 rounded-md opacity-30"></div>
</span>
</div>
</div>
{/if}
{:else}
<Chat
character={createSimpleCharacter(DBState.db.characters[$selectedCharID])}
idx={chat.index}
name={chat.name ?? currentUsername}
message={chat.data}
img={$ConnectionOpenStore ? '' : getCharImage(userIcon, 'css')}
isLastMemory={DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].lastMemory === (chat.chatId ?? 'none') && DBState.db.showMemoryLimit}
largePortrait={userIconProtrait}
messageGenerationInfo={chat.generationInfo}
/>
{/if}
{/each}
{/each}
{/key}
{#if DBState.db.characters[$selectedCharID].chats[DBState.db.characters[$selectedCharID].chatPage].message.length <= loadPages}
{#if DBState.db.characters[$selectedCharID].type !== 'group' }
<Chat