/**
* Paper Burner - KaTeX 渐进式渲染系统
* Phase 4.2+: 解决复杂消息首次渲染 4.6s 阻塞问题
*
* 核心策略:
* 1. 先渲染 Markdown(不渲染公式),立即显示内容框架
* 2. 公式位置放占位符
* 3. 使用 requestIdleCallback 分批渲染公式,避免阻塞主线程
*
* 预期效果:
* - 首次可见:< 300ms
* - 流式更新:每次 < 50ms
* - 用户感知:10倍速度提升
*/
(function() {
'use strict';
/**
* 渐进式渲染配置
*/
const PROGRESSIVE_CONFIG = {
BATCH_SIZE: 3, // 每批渲染的公式数量
IDLE_TIMEOUT: 50, // 空闲回调超时时间 (ms)
PLACEHOLDER_CLASS: 'katex-placeholder',
ENABLE: true // 是否启用渐进式渲染
};
/**
* 渲染队列管理器
*/
class KaTeXProgressiveRenderer {
constructor() {
this.renderQueue = []; // 待渲染的公式队列
this.isRendering = false; // 是否正在渲染
this.renderedCount = 0; // 已渲染数量
this.totalCount = 0; // 总公式数量
}
/**
* 第一步:快速渲染 Markdown(不渲染公式)
* 公式位置用占位符代替
*/
renderMarkdownWithPlaceholders(md) {
const formulas = [];
let formulaCounter = 0;
// 提取并替换 $$ ... $$ 块级公式
md = md.replace(/\$\$([\s\S]+?)\$\$/g, (match, tex) => {
const id = `katex-formula-${formulaCounter++}`;
formulas.push({
id: id,
tex: tex.trim(),
displayMode: true
});
return this.createPlaceholder(id, true);
});
// 提取并替换 \[ ... \] 块级公式
md = md.replace(/\\\[([\s\S]+?)\\\]/g, (match, tex) => {
const id = `katex-formula-${formulaCounter++}`;
formulas.push({
id: id,
tex: tex.trim(),
displayMode: true
});
return this.createPlaceholder(id, true);
});
// 提取并替换 $ ... $ 行内公式
md = md.replace(/\$([^\$]+?)\$/g, (match, tex) => {
const id = `katex-formula-${formulaCounter++}`;
formulas.push({
id: id,
tex: tex.trim(),
displayMode: false
});
return this.createPlaceholder(id, false);
});
// 提取并替换 \( ... \) 行内公式
md = md.replace(/\\\(([^)]+?)\\\)/g, (match, tex) => {
const id = `katex-formula-${formulaCounter++}`;
formulas.push({
id: id,
tex: tex.trim(),
displayMode: false
});
return this.createPlaceholder(id, false);
});
return { md, formulas };
}
/**
* 创建公式占位符
*/
createPlaceholder(id, displayMode) {
if (displayMode) {
return `\n
⏳ 渲染公式中...
\n`;
} else {
return `⏳`;
}
}
/**
* 第二步:将公式添加到渲染队列
*/
queueFormulas(formulas) {
this.renderQueue.push(...formulas);
this.totalCount = this.renderQueue.length;
this.renderedCount = 0;
// 如果还没开始渲染,启动渲染
if (!this.isRendering) {
this.startRendering();
}
}
/**
* 启动渐进式渲染
*/
startRendering() {
this.isRendering = true;
this.renderNextBatch();
}
/**
* 渲染下一批公式
*/
renderNextBatch() {
if (this.renderQueue.length === 0) {
this.isRendering = false;
console.log(`[KaTeX Progressive] ✅ 所有公式渲染完成 (${this.totalCount} 个)`);
return;
}
const startTime = performance.now();
// 取出一批公式
const batch = this.renderQueue.splice(0, PROGRESSIVE_CONFIG.BATCH_SIZE);
// 渲染这批公式
batch.forEach(formula => {
this.renderFormula(formula);
this.renderedCount++;
});
const duration = performance.now() - startTime;
// 性能监控
if (window.PerfMonitor) {
window.PerfMonitor.recordRender(duration, 'katex_progressive_batch');
}
// 使用 requestIdleCallback 或 setTimeout 调度下一批
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => this.renderNextBatch(), {
timeout: PROGRESSIVE_CONFIG.IDLE_TIMEOUT
});
} else {
setTimeout(() => this.renderNextBatch(), 0);
}
}
/**
* 渲染单个公式
*/
renderFormula(formula) {
const placeholder = document.getElementById(formula.id);
if (!placeholder) {
console.warn(`[KaTeX Progressive] 占位符未找到: ${formula.id}`);
return;
}
try {
const startTime = performance.now();
// 使用缓存渲染
const renderFn = window.renderKatexCached || katex.renderToString;
const html = renderFn(formula.tex, {
displayMode: formula.displayMode,
output: 'html',
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
});
const duration = performance.now() - startTime;
// 替换占位符
if (formula.displayMode) {
placeholder.outerHTML = `${html}
`;
} else {
placeholder.outerHTML = `${html}`;
}
// 性能监控
if (window.PerfMonitor && duration > 10) {
window.PerfMonitor.recordRender(duration, 'katex_progressive_single');
}
} catch (error) {
console.error(`[KaTeX Progressive] 渲染失败:`, formula.tex, error);
// 渲染失败时显示原始文本
placeholder.outerHTML = formula.displayMode
? ``
: `${formula.tex}`;
}
}
/**
* 清空队列
*/
clear() {
this.renderQueue = [];
this.isRendering = false;
this.renderedCount = 0;
this.totalCount = 0;
}
}
// 创建全局实例
window.KaTeXProgressiveRenderer = KaTeXProgressiveRenderer;
if (!window.katexProgressiveRenderer) {
window.katexProgressiveRenderer = new KaTeXProgressiveRenderer();
console.log('[KaTeX Progressive] 渐进式渲染系统已加载');
}
/**
* 替换原有的 renderWithKatexStreaming
* 使用渐进式渲染
*/
if (PROGRESSIVE_CONFIG.ENABLE && window.renderWithKatexStreaming) {
const originalRender = window.renderWithKatexStreaming;
window.renderWithKatexStreaming = function(md) {
const startTime = performance.now();
// 第一步:快速渲染(带占位符)
const { md: mdWithPlaceholders, formulas } = window.katexProgressiveRenderer.renderMarkdownWithPlaceholders(md);
// 渲染 Markdown(不包含公式)
let html;
if (typeof window.safeRenderMarkdown === 'function') {
html = window.safeRenderMarkdown(mdWithPlaceholders);
} else if (typeof marked !== 'undefined') {
html = marked.parse(mdWithPlaceholders);
} else {
html = mdWithPlaceholders;
}
const firstPassDuration = performance.now() - startTime;
console.log(`[KaTeX Progressive] 首次渲染完成: ${firstPassDuration.toFixed(1)}ms, 公式数: ${formulas.length}`);
// 第二步:将公式加入渲染队列(异步)
if (formulas.length > 0) {
// 使用 setTimeout 确保 DOM 已插入
setTimeout(() => {
window.katexProgressiveRenderer.queueFormulas(formulas);
}, 0);
}
return html;
};
console.log('[KaTeX Progressive] 已启用渐进式渲染模式');
}
})();