Skip to content

Commit 40e8a21

Browse files
committed
perf: throttle markdown lexing during streaming and reduce redundant re-renders
- Throttle marked.lexer() calls to ~100ms during streaming (done=false), flush immediately when streaming completes, preventing hundreds of full re-lexes per response especially after tool calls inflate content size - Memoize sourceIds computation in ContentRenderer as a reactive variable instead of recomputing inline on every render, preventing unnecessary child tree re-renders during streaming - Pre-compute marked.lexer() for details block content using @const instead of calling it inline in the template, eliminating redundant lexing on every parent re-render
1 parent 2ef2b8b commit 40e8a21

3 files changed

Lines changed: 67 additions & 32 deletions

File tree

src/lib/components/chat/Messages/ContentRenderer.svelte

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,39 @@
4040
export let onTaskClick = (e) => {};
4141
export let onAddMessages = (e) => {};
4242
43+
// Memoize sourceIds to avoid creating a new array reference on every render,
44+
// which would cause the entire Markdown/MarkdownTokens tree to re-render
45+
$: computedSourceIds = (sources ?? []).reduce((acc, source) => {
46+
let ids = [];
47+
source.document.forEach((document, index) => {
48+
if (model?.info?.meta?.capabilities?.citations == false) {
49+
ids.push('N/A');
50+
return ids;
51+
}
52+
53+
const metadata = source.metadata?.[index];
54+
const id = metadata?.source ?? 'N/A';
55+
56+
if (metadata?.name) {
57+
ids.push(metadata.name);
58+
return ids;
59+
}
60+
61+
if (id.startsWith('http://') || id.startsWith('https://')) {
62+
ids.push(id);
63+
} else {
64+
ids.push(source?.source?.name ?? id);
65+
}
66+
67+
return ids;
68+
});
69+
70+
acc = [...acc, ...ids];
71+
72+
// remove duplicates
73+
return acc.filter((item, index) => acc.indexOf(item) === index);
74+
}, []);
75+
4376
let contentContainerElement;
4477
let floatingButtonsElement;
4578
@@ -143,36 +176,7 @@
143176
{done}
144177
{editCodeBlock}
145178
{topPadding}
146-
sourceIds={(sources ?? []).reduce((acc, source) => {
147-
let ids = [];
148-
source.document.forEach((document, index) => {
149-
if (model?.info?.meta?.capabilities?.citations == false) {
150-
ids.push('N/A');
151-
return ids;
152-
}
153-
154-
const metadata = source.metadata?.[index];
155-
const id = metadata?.source ?? 'N/A';
156-
157-
if (metadata?.name) {
158-
ids.push(metadata.name);
159-
return ids;
160-
}
161-
162-
if (id.startsWith('http://') || id.startsWith('https://')) {
163-
ids.push(id);
164-
} else {
165-
ids.push(source?.source?.name ?? id);
166-
}
167-
168-
return ids;
169-
});
170-
171-
acc = [...acc, ...ids];
172-
173-
// remove duplicates
174-
return acc.filter((item, index) => acc.indexOf(item) === index);
175-
}, [])}
179+
sourceIds={computedSourceIds}
176180
{onSourceClick}
177181
{onTaskClick}
178182
{onSave}

src/lib/components/chat/Messages/Markdown.svelte

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
</script>
2525

2626
<script>
27+
import { onDestroy } from 'svelte';
2728
import { replaceTokens, processResponseContent } from '$lib/utils';
2829
import { user } from '$lib/stores';
2930
@@ -52,11 +53,40 @@
5253
5354
let tokens = [];
5455
55-
$: if (content) {
56+
const THROTTLE_MS = 100;
57+
let throttleTimer = null;
58+
let lastLexTime = 0;
59+
60+
function lexContent(content) {
5661
tokens = marked.lexer(
5762
replaceTokens(processResponseContent(content), model?.name, $user?.name)
5863
);
64+
lastLexTime = Date.now();
5965
}
66+
67+
$: if (content) {
68+
if (done) {
69+
// When streaming is complete, always lex immediately
70+
clearTimeout(throttleTimer);
71+
throttleTimer = null;
72+
lexContent(content);
73+
} else {
74+
// During streaming, throttle to reduce processing
75+
const elapsed = Date.now() - lastLexTime;
76+
if (elapsed >= THROTTLE_MS) {
77+
lexContent(content);
78+
} else if (!throttleTimer) {
79+
throttleTimer = setTimeout(() => {
80+
throttleTimer = null;
81+
lexContent(content);
82+
}, THROTTLE_MS - elapsed);
83+
}
84+
}
85+
}
86+
87+
onDestroy(() => {
88+
clearTimeout(throttleTimer);
89+
});
6090
</script>
6191
6292
{#key id}

src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@
325325
{@const textContent = decode(token.text || '')
326326
.replace(/<summary>.*?<\/summary>/gi, '')
327327
.trim()}
328+
{@const detailsTokens = textContent.length > 0 ? marked.lexer(decode(token.text)) : []}
328329

329330
{#if token?.attributes?.type === 'tool_calls'}
330331
<!-- Tool calls have dedicated handling with ToolCallDisplay component -->
@@ -345,7 +346,7 @@
345346
<div class=" mb-1.5" slot="content">
346347
<svelte:self
347348
id={`${id}-${tokenIdx}-d`}
348-
tokens={marked.lexer(decode(token.text))}
349+
tokens={detailsTokens}
349350
attributes={token?.attributes}
350351
{done}
351352
{editCodeBlock}

0 commit comments

Comments
 (0)