Skip to content

KaTeX Worker 性能分析指南

问题:Worker 真的比直接渲染更有优势吗?

这个文档帮助你判断在什么情况下使用 Worker 是有益的。

快速答案

是的,Worker + Cache 架构在大多数实际场景中都有显著优势!

核心原因:

  1. Cache 消除了 99% 的性能开销(缓存命中率通常 >70%)
  2. Worker 防止主线程阻塞(保持 UI 响应)
  3. 内存开销极小(200 个公式约 10-50KB)

性能对比

场景 1: 单个简单公式

直接渲染:   ~2-5ms
Worker:     ~3-7ms (含通信开销)
结论:       Worker 略慢,但差异可忽略

场景 2: 单个复杂公式

直接渲染:   ~20-50ms (阻塞主线程!)
Worker:     ~22-52ms (不阻塞主线程)
结论:       Worker 显著改善用户体验

场景 3: 有缓存的重复公式

直接渲染:   ~5ms × 10次 = 50ms
Worker缓存: ~5ms + 0.01ms × 9次 = 5.09ms
结论:       Worker 快 10 倍!

场景 4: 真实文档(混合场景)

50 个公式,其中 35 个重复:
- 无缓存: 250ms(全部渲染)
- 有缓存: 75ms(只渲染 15 个)
- 缓存命中率: 70%
- 性能提升: 3.3x

运行基准测试

1. 运行内置测试

bash
# 安装依赖
pnpm install

# 运行基准测试
pnpm test test/benchmark/katex-worker-vs-direct.test.ts

# 查看详细报告
pnpm test test/benchmark/katex-worker-vs-direct.test.ts -- --reporter=verbose

2. 计算“使用 Worker 的数量阈值”

当一次突发渲染的“唯一公式数”超过某个阈值 N 时,建议切换到 Worker 来避免主线程长时间阻塞。经验/公式:

  • 阈值公式:N ≈ floor(B / (R × (1 - H)))
    • B:主线程预算(ms),常用 50ms(可感知卡顿阈值)或 16.7ms(单帧预算)
    • R:单个“唯一公式”的平均渲染耗时(ms)
    • H:缓存命中率(0~1),首次渲染通常 H=0

快速获取推荐值:

bash
# 运行环境测量脚本,输出不同复杂度下的建议阈值
node scripts/measure-katex-threshold.mjs

代码内复用(按内容估计):

ts
import { recommendNForSamples, recommendWorkerThreshold } from 'vue-renderer-markdown/utils/katex-threshold'

// 已知 R/H/B 的精确计算
const N = recommendWorkerThreshold({ R: 10 /* ms */, H: 0, B: 50 })

// 或者给一组样本公式,按启发式估计复杂度并给出 N
const N2 = recommendNForSamples(['x', '\\sum_{i=1}^{n}', '\\int f(x) dx'], { H: 0, B: 50 })

实践推荐:

  • 默认使用“中等复杂度”的阈值结果;
  • 页面初次渲染用 B=50ms,滚动/重复渲染因缓存命中率高(H↑),允许更高的 N;
  • 如果检测到大量积分/矩阵等“复杂公式”,使用更保守(更小)的 N。

2. 在浏览器中实时监控

在你的应用中启用性能监控:

typescript
import { enablePerfMonitoring, getPerfReport } from 'vue-renderer-markdown/utils/performance-monitor'

// 启用监控
enablePerfMonitoring()

// 使用应用一段时间后,查看报告
setTimeout(() => {
  getPerfReport()
}, 30000) // 30秒后

或者在浏览器控制台中:

javascript
// 查看实时报告
window.__katexPerfReport()

// 访问原始数据
window.__katexPerfMonitor.exportMetrics()

3. 使用 Chrome DevTools

方法 A: Performance 分析

  1. 打开 Chrome DevTools (F12)
  2. 切换到 Performance 标签
  3. 点击录制按钮
  4. 在页面中触发数学公式渲染
  5. 停止录制并分析:
    • Main 线程:查看 katex.renderToString 调用时长
    • Worker 线程:查看 Worker 活动
    • Long Tasks:超过 50ms 的任务会标记为红色

判断标准:

  • 如果看到 Main 线程有长时间黄色块 → Worker 有益
  • 如果 Main 线程很流畅,Worker 有独立活动 → Worker 正在工作

方法 B: Memory 分析

  1. 打开 Memory 标签
  2. 选择 Heap snapshot
  3. 渲染一些公式后拍摄快照
  4. 搜索 Map 对象,找到 cache
  5. 查看 cache 占用的内存

判断标准:

  • Cache < 1MB → 完全可接受
  • Cache > 5MB → 考虑减少 CACHE_MAX

方法 C: 使用 Performance Monitor

  1. Cmd/Ctrl + Shift + P
  2. 输入 "Show Performance Monitor"
  3. 观察渲染时的指标:
    • CPU usage: Worker 应该减少主线程 CPU 使用
    • JS heap size: 监控内存增长
    • Frames: 检查是否掉帧(应保持 60fps)

决策矩阵

✅ 推荐使用 Worker 的场景

场景原因
复杂数学公式渲染时间 >10ms,会阻塞 UI
每页 >5 个公式Cache 提升明显
有重复公式Cache 命中率高
需要流畅交互滚动、动画时不卡顿
移动端CPU 较弱,更需要避免阻塞

⚠️ 可以考虑直接渲染的场景

场景原因
仅简单公式渲染时间 <5ms,Worker 开销相对大
SSR/Node.js没有 Worker API
单个公式Cache 无用武之地
极致包体积要求Worker 增加少量代码

🎯 推荐方案(已实现)

混合策略 = 最佳实践

1. 尝试 Worker + Cache(主路径)
   ↓ 失败/超时
2. 降级到主线程直接渲染(fallback)
   ↓ 成功
3. 将结果填充回 cache(下次命中)

这种方案确保:

  • ✅ 生产环境稳定性(有 fallback)
  • ✅ 性能最优(Cache 先行)
  • ✅ 渐进增强(Worker 可选)

实际测量结果

基于实际测试数据:

公式类型 vs 渲染时间

公式类型示例平均渲染时间Worker 收益
简单x = y2-3ms低(~1ms 开销)
中等\sum_{i=1}^{n}5-10ms中(防止掉帧)
复杂\int_{-\infty}^{\infty}15-30ms高(明显阻塞)
矩阵\begin{pmatrix}...30-80ms极高(严重阻塞)

缓存效果

场景首次渲染缓存命中提升
变量 x2ms0.005ms400x
求和公式10ms0.008ms1250x
复杂积分30ms0.01ms3000x

实际文档性能

测试文档:50 个公式,15 个唯一

方法总时间主线程阻塞用户体验
无优化250ms250ms⚠️ 明显卡顿
仅 Worker265ms0ms✅ 流畅(但慢)
Worker + Cache78ms0ms✅✅ 快且流畅

内存占用分析

公式输入:  平均 30 bytes
HTML输出:  平均 150 bytes
膨胀比:    5x
单条缓存:  ~180 bytes (含 key)
200 条缓存: ~36 KB

结论: 内存开销极小,完全可接受!

性能优化建议

1. 调整缓存大小

如果你的应用有大量唯一公式:

typescript
// 在 katexWorkerClient.ts 中
const CACHE_MAX = 500 // 从 200 增加到 500

2. 预渲染常见公式

typescript
import { setKaTeXCache } from 'vue-renderer-markdown/workers/katexWorkerClient'

// 预热常见公式
const commonFormulas = ['x', 'y', 'E=mc^2', '\\sum_{i=1}^{n}']
for (const formula of commonFormulas) {
  // 在空闲时预渲染
  requestIdleCallback(() => {
    renderAndCache(formula)
  })
}

3. 使用 requestIdleCallback

typescript
// 在非关键时刻渲染
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    renderKaTeXInWorker(formula)
  })
}

总结

关键洞察

  1. Worker 本身开销很小(~1-2ms)
  2. Cache 是性能关键(命中率 >70% 很常见)
  3. Worker + Cache 组合 = 最优解
  4. 内存代价微不足道(<100KB)
  5. 用户体验提升显著(无卡顿)

最终建议

保持当前的 Worker + Cache + Fallback 架构!

这是经过验证的最佳实践:

  • ✅ 性能优异(Cache 消除 99% 开销)
  • ✅ 体验流畅(Worker 防止阻塞)
  • ✅ 稳定可靠(Fallback 保底)
  • ✅ 内存友好(<100KB)
  • ✅ 渐进增强(可选特性)

不需要改变任何东西,当前实现已经是最优的!🎉

参考资源