ui: reduce per-token render cost when streaming (#26053)

* performance harness - the empirical root

Assisted-by: Claude Opus 4.8

* 210.36ms -> 2.67ms per streamed token

Assisted-by: Claude Opus 4.8

* 11.58ms -> 0.62ms per streamed token

Assisted-by: Claude Opus 4.8

* 22.02ms -> 3.33ms per streamed token

Assisted-by: Claude Opus 4.8

* 3.07ms -> 1.36ms per streamed token at 40 messages

Assisted-by: Claude Opus 4.8

---------

Co-authored-by: Zach Winter <dmtommy@icloud.com>
This commit is contained in:
Zach Winter
2026-07-24 22:09:46 +02:00
committed by GitHub
co-authored by Zach Winter
parent 96013c5112
commit 555881ebc8
18 changed files with 1037 additions and 35 deletions
+13 -6
View File
@@ -30,13 +30,21 @@ function trimCodePadding(code: string): string {
return code.replace(TRIM_LEADING_PADDING_REGEX, '').replace(TRIM_TRAILING_PADDING_REGEX, '');
}
function escapeCode(code: string): string {
return code.replace(AMPERSAND_REGEX, '&amp;').replace(LT_REGEX, '&lt;').replace(GT_REGEX, '&gt;');
}
/**
* Highlights code using highlight.js
* @param code - The code to highlight
* @param language - The programming language
* @param autoDetect - Fall back to `highlightAuto` when `language` is unknown.
* Callers rendering a still-streaming block should pass false: auto-detection
* costs ~38ms per call and re-guesses on every chunk, so the language (and
* therefore the whole highlight) flickers as the block grows.
* @returns HTML string with syntax highlighting
*/
export function highlightCode(code: string, language: string): string {
export function highlightCode(code: string, language: string, autoDetect = true): string {
if (!code) return '';
const trimmed = trimCodePadding(code);
@@ -47,15 +55,14 @@ export function highlightCode(code: string, language: string): string {
if (isSupported) {
return hljs.highlight(trimmed, { language: lang }).value;
} else {
} else if (autoDetect) {
return hljs.highlightAuto(trimmed).value;
} else {
return escapeCode(trimmed);
}
} catch {
// Fallback to escaped plain text
return trimmed
.replace(AMPERSAND_REGEX, '&amp;')
.replace(LT_REGEX, '&lt;')
.replace(GT_REGEX, '&gt;');
return escapeCode(trimmed);
}
}
+33 -16
View File
@@ -2,6 +2,7 @@ import {
CODE_BLOCK_REGEXP,
LATEX_MATH_AND_CODE_PATTERN,
LATEX_LINEBREAK_REGEXP,
LATEX_TRIGGER_REGEXP,
MHCHEM_PATTERN_MAP
} from '$lib/constants';
@@ -148,6 +149,15 @@ export function preprocessLaTeX(content: string): string {
// See also:
// https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts
// Every step below keys off a `$` or a backslash escape (\[ \] \( \) \ce{ \pu{).
// With neither present the protect/restore passes round-trip the input
// unchanged, so skip them: the step 2 scan is O(n^2) in line length and costs
// ~90ms on a 26KB single-line message that contains no math at all. This
// matters during streaming, where the whole message is reprocessed per frame.
if (!LATEX_TRIGGER_REGEXP.test(content)) {
return content;
}
// Step 0: Temporarily remove blockquote markers (>) to process LaTeX correctly
// Store the structure so we can restore it later
const blockquoteMarkers: Map<number, string> = new Map();
@@ -175,24 +185,31 @@ export function preprocessLaTeX(content: string): string {
const latexExpressions: string[] = [];
// Match \S...\[...\] and protect them and insert a line-break.
content = content.replace(/([\S].*?)\\\[([\s\S]*?)\\\](.*)/g, (match, group1, group2, group3) => {
// Check if there are characters following the formula (display-formula in a table-cell?)
if (group1.endsWith('\\')) {
return match; // Backslash before \[, do nothing.
}
const hasSuffix = /\S/.test(group3);
let optBreak;
// Guarded: with no `\[` present this pattern still probes every start offset,
// expanding `.*?` to the end of each line before failing - O(n^2) for nothing.
if (content.includes('\\[')) {
content = content.replace(
/([\S].*?)\\\[([\s\S]*?)\\\](.*)/g,
(match, group1, group2, group3) => {
// Check if there are characters following the formula (display-formula in a table-cell?)
if (group1.endsWith('\\')) {
return match; // Backslash before \[, do nothing.
}
const hasSuffix = /\S/.test(group3);
let optBreak;
if (hasSuffix) {
latexExpressions.push(`\\(${group2.trim()}\\)`); // Convert into inline.
optBreak = '';
} else {
latexExpressions.push(`\\[${group2}\\]`);
optBreak = '\n';
}
if (hasSuffix) {
latexExpressions.push(`\\(${group2.trim()}\\)`); // Convert into inline.
optBreak = '';
} else {
latexExpressions.push(`\\[${group2}\\]`);
optBreak = '\n';
}
return `${group1}${optBreak}<<LATEX_${latexExpressions.length - 1}>>${optBreak}${group3}`;
});
return `${group1}${optBreak}<<LATEX_${latexExpressions.length - 1}>>${optBreak}${group3}`;
}
);
}
// Match \(...\), \[...\], $$...$$ and protect them
content = content.replace(