284 lines
6.6 KiB
JavaScript
284 lines
6.6 KiB
JavaScript
/**
|
||
* Paper Burner - KaTeX 公式缓存系统
|
||
* Phase 4.2+: 解决公式渲染阻塞主线程问题(实测 4.6s 阻塞)
|
||
*
|
||
* 功能:
|
||
* - 缓存已渲染的公式,避免重复渲染
|
||
* - LRU 策略,限制缓存大小
|
||
* - 自动清理过期缓存
|
||
* - 性能监控集成
|
||
*
|
||
* 预期收益:
|
||
* - 重复公式渲染时间减少 99%(从 50ms 降至 0.5ms)
|
||
* - 充满公式的聊天打开时间从 4.6s 降至 0.5s 以下
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* LRU 缓存实现
|
||
*/
|
||
class LRUCache {
|
||
constructor(maxSize = 1000) {
|
||
this.maxSize = maxSize;
|
||
this.cache = new Map();
|
||
this.stats = {
|
||
hits: 0,
|
||
misses: 0,
|
||
evictions: 0,
|
||
totalRenderTime: 0,
|
||
totalCacheTime: 0
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取缓存项
|
||
*/
|
||
get(key) {
|
||
const startTime = performance.now();
|
||
|
||
if (!this.cache.has(key)) {
|
||
this.stats.misses++;
|
||
return null;
|
||
}
|
||
|
||
// LRU: 移到末尾(最近使用)
|
||
const value = this.cache.get(key);
|
||
this.cache.delete(key);
|
||
this.cache.set(key, value);
|
||
|
||
this.stats.hits++;
|
||
this.stats.totalCacheTime += performance.now() - startTime;
|
||
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* 设置缓存项
|
||
*/
|
||
set(key, value) {
|
||
// 如果已存在,先删除
|
||
if (this.cache.has(key)) {
|
||
this.cache.delete(key);
|
||
}
|
||
|
||
// 如果超过最大容量,删除最老的项
|
||
if (this.cache.size >= this.maxSize) {
|
||
const firstKey = this.cache.keys().next().value;
|
||
this.cache.delete(firstKey);
|
||
this.stats.evictions++;
|
||
}
|
||
|
||
this.cache.set(key, value);
|
||
}
|
||
|
||
/**
|
||
* 清空缓存
|
||
*/
|
||
clear() {
|
||
this.cache.clear();
|
||
this.stats.hits = 0;
|
||
this.stats.misses = 0;
|
||
this.stats.evictions = 0;
|
||
this.stats.totalRenderTime = 0;
|
||
this.stats.totalCacheTime = 0;
|
||
}
|
||
|
||
/**
|
||
* 获取统计信息
|
||
*/
|
||
getStats() {
|
||
const total = this.stats.hits + this.stats.misses;
|
||
const hitRate = total > 0 ? (this.stats.hits / total * 100) : 0;
|
||
|
||
return {
|
||
size: this.cache.size,
|
||
maxSize: this.maxSize,
|
||
hits: this.stats.hits,
|
||
misses: this.stats.misses,
|
||
evictions: this.stats.evictions,
|
||
hitRate: hitRate.toFixed(1) + '%',
|
||
avgRenderTime: this.stats.misses > 0
|
||
? (this.stats.totalRenderTime / this.stats.misses).toFixed(2) + 'ms'
|
||
: '0ms',
|
||
avgCacheTime: this.stats.hits > 0
|
||
? (this.stats.totalCacheTime / this.stats.hits).toFixed(3) + 'ms'
|
||
: '0ms'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* KaTeX 缓存管理器
|
||
*/
|
||
class KaTeXCache {
|
||
constructor(options = {}) {
|
||
this.maxSize = options.maxSize || 1000;
|
||
this.enableMonitoring = options.enableMonitoring !== false;
|
||
this.cache = new LRUCache(this.maxSize);
|
||
|
||
// KaTeX 渲染选项的默认值
|
||
this.defaultOptions = {
|
||
strict: 'ignore',
|
||
throwOnError: false,
|
||
trust: false,
|
||
macros: {},
|
||
maxSize: 50,
|
||
maxExpand: 100
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 生成缓存键
|
||
* 包含公式内容和渲染选项
|
||
*/
|
||
generateKey(tex, options) {
|
||
const displayMode = options.displayMode ? '1' : '0';
|
||
const output = options.output || 'html';
|
||
|
||
// 只包含影响渲染结果的选项
|
||
return `${displayMode}:${output}:${tex}`;
|
||
}
|
||
|
||
/**
|
||
* 渲染公式(带缓存)
|
||
*/
|
||
render(tex, options = {}) {
|
||
// 合并选项
|
||
const renderOptions = { ...this.defaultOptions, ...options };
|
||
|
||
// 生成缓存键
|
||
const cacheKey = this.generateKey(tex, renderOptions);
|
||
|
||
// 检查缓存
|
||
const cached = this.cache.get(cacheKey);
|
||
if (cached !== null) {
|
||
return cached;
|
||
}
|
||
|
||
// 缓存未命中,渲染公式
|
||
const startTime = performance.now();
|
||
|
||
let result;
|
||
try {
|
||
result = katex.renderToString(tex, renderOptions);
|
||
} catch (error) {
|
||
// 渲染失败也缓存,避免重复尝试
|
||
result = null;
|
||
}
|
||
|
||
const duration = performance.now() - startTime;
|
||
this.cache.stats.totalRenderTime += duration;
|
||
|
||
// 性能监控
|
||
if (this.enableMonitoring && window.PerfMonitor) {
|
||
window.PerfMonitor.recordRender(duration, 'katex_render');
|
||
}
|
||
|
||
// 存入缓存
|
||
this.cache.set(cacheKey, result);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 批量预渲染公式
|
||
* 用于聊天历史加载时提前渲染常见公式
|
||
*/
|
||
async preRender(formulas, options = {}) {
|
||
const results = [];
|
||
|
||
for (const tex of formulas) {
|
||
const result = this.render(tex, options);
|
||
results.push({ tex, result });
|
||
|
||
// 避免阻塞主线程太久
|
||
if (results.length % 10 === 0) {
|
||
await new Promise(resolve => setTimeout(resolve, 0));
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 获取缓存统计
|
||
*/
|
||
getStats() {
|
||
return this.cache.getStats();
|
||
}
|
||
|
||
/**
|
||
* 清空缓存
|
||
*/
|
||
clear() {
|
||
this.cache.clear();
|
||
}
|
||
|
||
/**
|
||
* 导出缓存(用于持久化)
|
||
*/
|
||
export() {
|
||
const entries = Array.from(this.cache.cache.entries());
|
||
return {
|
||
version: 1,
|
||
timestamp: Date.now(),
|
||
entries: entries,
|
||
stats: this.cache.stats
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 导入缓存(从持久化恢复)
|
||
*/
|
||
import(data) {
|
||
if (!data || data.version !== 1) {
|
||
console.warn('[KaTeXCache] Invalid cache data');
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
this.cache.cache.clear();
|
||
data.entries.forEach(([key, value]) => {
|
||
this.cache.cache.set(key, value);
|
||
});
|
||
return true;
|
||
} catch (error) {
|
||
console.error('[KaTeXCache] Failed to import cache:', error);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建全局实例
|
||
window.KaTeXCache = KaTeXCache;
|
||
|
||
// 自动初始化默认实例
|
||
if (!window.katexCache) {
|
||
window.katexCache = new KaTeXCache({
|
||
maxSize: 1000,
|
||
enableMonitoring: true
|
||
});
|
||
|
||
console.log('[KaTeXCache] Formula cache initialized with maxSize: 1000');
|
||
}
|
||
|
||
// 提供全局便捷方法
|
||
window.renderKatexCached = function(tex, options) {
|
||
return window.katexCache.render(tex, options);
|
||
};
|
||
|
||
// 在控制台提供统计查看方法
|
||
window.getKatexCacheStats = function() {
|
||
const stats = window.katexCache.getStats();
|
||
console.log('KaTeX 缓存统计:');
|
||
console.table(stats);
|
||
return stats;
|
||
};
|
||
})();
|
||
|
||
console.log('[KaTeXCache] KaTeX formula cache system loaded');
|
||
console.log(' Usage: renderKatexCached(tex, options)');
|
||
console.log(' Stats: getKatexCacheStats()');
|