perf(ui): reuse warm markdown blocks

This commit is contained in:
c_w_xiaohei
2026-08-26 00:42:36 +08:00
parent 26dbc2f309
commit 25be29731f
4 changed files with 592 additions and 33 deletions
@@ -49,7 +49,10 @@ import { escapeRawMarkdownHtml, isLocalFileUrl, MARKDOWN_FORBIDDEN_TAGS } from '
const {
__markdownImageCandidateCacheForTests,
extractMarkdownImageCandidates,
getCachedMarkdownBlocks,
renderMarkdownBlocks,
renderMarkdownSync,
resetMarkdownHtmlCacheForTests,
} = await import('./markdownCore');
const { resolveMarkdownImageSource } = await import('./markdownImageAssets');
@@ -90,6 +93,47 @@ describe('markdown sanitization', () => {
});
describe('Markdown block cache reads', () => {
test('returns all settled blocks synchronously after a full cache hit', async () => {
resetMarkdownHtmlCacheForTests();
const text = '**cached** settled markdown';
expect(getCachedMarkdownBlocks(text)).toBeNull();
const rendered = await renderMarkdownBlocks(text, false);
expect(getCachedMarkdownBlocks(text)).toEqual(rendered);
});
test('returns null for a cold or partial settled miss', async () => {
resetMarkdownHtmlCacheForTests();
const first = 'first settled block';
const changed = 'first settled block\n\nsecond settled block';
await renderMarkdownBlocks(first, false);
expect(getCachedMarkdownBlocks(changed)).toBeNull();
});
test('keeps image mode identity out of the settled full hit', async () => {
resetMarkdownHtmlCacheForTests();
const text = '![image](https://example.test/image.png)';
await renderMarkdownBlocks(text, false, 'inline');
expect(getCachedMarkdownBlocks(text, 'label')).toBeNull();
expect(getCachedMarkdownBlocks(text, 'inline')).not.toBeNull();
});
test('does not treat streaming live-cache entries as settled full hits', async () => {
resetMarkdownHtmlCacheForTests();
const text = 'streaming markdown';
await renderMarkdownBlocks(text, true);
expect(getCachedMarkdownBlocks(text)).toBeNull();
});
});
describe('Markdown images', () => {
test('renders assistant images as icon-ready text without loading the source', () => {
const html = renderMarkdownSync([
@@ -548,10 +548,30 @@ export const __markdownBlockCacheSizesForTests = (): { full: number; live: numbe
live: liveBlockCache.size,
});
const parseBlock = async (
block: MarkdownBlock,
imageMode: MarkdownImageMode,
): Promise<string> => {
/**
* Read a settled render synchronously when every block is already in the full
* cache. Cache reads retain the existing LRU `get` semantics and do not insert
* or expand either cache.
*/
export const getCachedMarkdownBlocks = (
text: string,
imageMode: MarkdownImageMode = 'inline',
): RenderedBlock[] | null => {
if (!text) return [];
const blocks = streamBlocks(text, false);
const rendered: RenderedBlock[] = [];
for (const block of blocks) {
const contentHash = contentFingerprint(block.raw);
const id = markdownBlockCacheKey(contentHash, block.mode, block.highlight, imageMode);
const html = fullBlockCache.get(id);
if (html === undefined) return null;
rendered.push({ id, html });
}
return rendered;
};
const parseBlock = async (block: MarkdownBlock, imageMode: MarkdownImageMode): Promise<string> => {
const parser = imageMode === 'label' ? imageLabelParser : inlineImageParser;
const parsed = await Promise.resolve(parser.parse(block.src));
const withMath = renderMathExpressions(parsed);