/** * 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()');