e0a5f57793
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
260 lines
7.6 KiB
Markdown
260 lines
7.6 KiB
Markdown
# KaTeX Worker Performance Playbook
|
|
|
|
> Looking for the Chinese version? See [KaTeX Worker Performance Guide (ZH)](/zh/guide/katex-worker-performance-analysis).
|
|
|
|
## Question: is a Worker actually faster than rendering on the main thread?
|
|
|
|
Use this guide to decide when the Worker + cache pipeline is worth enabling.
|
|
|
|
## Short answer
|
|
|
|
**Yes. A Worker backed by a cache easily wins in most real workloads.**
|
|
|
|
Why:
|
|
1. **The cache eliminates ~99 % of the cost** (cache hit rate commonly >70 %).
|
|
2. **The Worker keeps the main thread responsive**, so scrolling/typing stays smooth even when formulas are heavy.
|
|
3. **Memory overhead is tiny** -- roughly 10-50 KB for ~200 cached formulas.
|
|
|
|
## Quick performance comparisons
|
|
|
|
### Scenario 1 - single lightweight formula
|
|
```
|
|
Direct render: ~2-5 ms
|
|
Worker: ~3-7 ms (includes postMessage overhead)
|
|
Takeaway: Worker is slightly slower but the difference is negligible.
|
|
```
|
|
|
|
### Scenario 2 - single complex formula
|
|
```
|
|
Direct render: ~20-50 ms (blocks the main thread)
|
|
Worker: ~22-52 ms (main thread stays free)
|
|
Takeaway: UX improves because the page never freezes.
|
|
```
|
|
|
|
### Scenario 3 - repeated formula with cache
|
|
```
|
|
Direct render: 5 ms x 10 renders = 50 ms
|
|
Worker+cache: 5 ms + 0.01 ms x 9 hits = 5.09 ms
|
|
Takeaway: ~10x faster once cached.
|
|
```
|
|
|
|
### Scenario 4 - mixed real document
|
|
```
|
|
50 formulas with 35 duplicates:
|
|
- No cache: 250 ms (every formula rerenders)
|
|
- With cache: 75 ms (only 15 "unique" renders)
|
|
- Cache hit rate: 70 %
|
|
- Speedup: ~3.3x
|
|
```
|
|
|
|
## How to benchmark
|
|
|
|
### 1. Use the built-in Vitest benchmark
|
|
|
|
```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. Estimate the "switch to Worker" threshold
|
|
|
|
Compute how many unique formulas (N) you can render on the main thread before risking a noticeable jank:
|
|
|
|
- Formula: `N ~ floor(B / (R x (1 - H)))`
|
|
- `B`: main-thread budget in ms (use 50 ms for "user sees a hitch" or 16.7 ms for 1 frame).
|
|
- `R`: average time to render one unique formula.
|
|
- `H`: cache hit rate (0-1). When first rendering a page, assume `H = 0`.
|
|
|
|
Fast helpers:
|
|
|
|
```bash
|
|
node scripts/measure-katex-threshold.mjs
|
|
```
|
|
|
|
```ts
|
|
import { recommendNForSamples, recommendWorkerThreshold } from 'markstream-vue/utils/katex-threshold'
|
|
|
|
const exactN = recommendWorkerThreshold({ R: 10, H: 0, B: 50 })
|
|
const sampleBased = recommendNForSamples(['x', '\\sum_{i=1}^{n}', '\\int f(x) dx'], { H: 0, B: 50 })
|
|
```
|
|
|
|
Practical tips:
|
|
- Default to the "medium complexity" threshold.
|
|
- First paint: assume `B = 50` and `H = 0`. During scrolling or repeat renders, increase `N` because the cache hit rate climbs quickly.
|
|
- If you detect lots of integrals/matrices, pick the conservative threshold (smaller `N`).
|
|
|
|
### 3. Monitor live traffic
|
|
|
|
```ts
|
|
import { enablePerfMonitoring, getPerfReport } from 'markstream-vue/utils/performance-monitor'
|
|
|
|
enablePerfMonitoring()
|
|
|
|
setTimeout(() => {
|
|
getPerfReport()
|
|
}, 30_000)
|
|
```
|
|
|
|
Browser console helpers:
|
|
|
|
```js
|
|
window.__katexPerfReport()
|
|
window.__katexPerfMonitor.exportMetrics()
|
|
```
|
|
|
|
### 4. Inspect Chrome DevTools
|
|
|
|
#### A. Performance panel
|
|
1. Open DevTools -> **Performance**.
|
|
2. Record while rendering formulas.
|
|
3. Inspect:
|
|
- **Main** lane -> watch `katex.renderToString`.
|
|
- **Worker** lane -> ensure work moved off the main thread.
|
|
- **Long tasks** (>50 ms) -> any red markers mean main thread was blocked.
|
|
|
|
#### B. Memory panel
|
|
1. Take a **Heap snapshot** after rendering.
|
|
2. Search for the cache `Map`.
|
|
3. Check size:
|
|
- <1 MB -> no worries.
|
|
- >5 MB -> lower `CACHE_MAX`.
|
|
|
|
#### C. Performance Monitor
|
|
1. Cmd/Ctrl + Shift + P -> "Show Performance Monitor".
|
|
2. Watch **CPU usage**, **JS heap**, **Frames** while rendering.
|
|
|
|
## Decision matrix
|
|
|
|
### :white_check_mark: When to prefer Worker + cache
|
|
|
|
| Scenario | Rationale |
|
|
| --- | --- |
|
|
| Complex math (>10 ms) | Keeps UI responsive. |
|
|
| >5 formulas per page | Cache savings stack up. |
|
|
| Lots of repetitions | Cache hit rate skyrockets. |
|
|
| Smooth scrolling/typing required | No main-thread stalls. |
|
|
| Mobile devices | CPUs are weaker, so avoid blocking. |
|
|
|
|
### :warning: When direct render is fine
|
|
|
|
| Scenario | Rationale |
|
|
| --- | --- |
|
|
| Only trivial formulas | <5 ms each, Worker overhead similar. |
|
|
| SSR / Node.js | Worker API unavailable. |
|
|
| Single formula | Cache never pays off. |
|
|
| Extreme bundle constraints | Worker adds a small chunk. |
|
|
|
|
### :dart: Recommended pipeline (already implemented)
|
|
|
|
```
|
|
try Worker + cache
|
|
-> on error / timeout
|
|
fallback to direct render
|
|
-> on success
|
|
store result back in cache
|
|
```
|
|
|
|
Benefits:
|
|
- :white_check_mark: Production-safe (there is always a fallback).
|
|
- :white_check_mark: Fast path takes advantage of caching.
|
|
- :white_check_mark: Progressive enhancement (Worker is optional).
|
|
|
|
## Real-world measurements
|
|
|
|
### Render time by formula type
|
|
|
|
| Type | Example | Avg time | Worker benefit |
|
|
| --- | --- | --- | --- |
|
|
| Simple | `x = y` | 2-3 ms | Low (~1 ms overhead). |
|
|
| Medium | `\sum_{i=1}^{n}` | 5-10 ms | Medium (prevents frame drops). |
|
|
| Complex | `\int_{-\infty}^{\infty}` | 15-30 ms | High (avoids jank). |
|
|
| Matrix | `\begin{pmatrix}...` | 30-80 ms | Huge (main thread unusable otherwise). |
|
|
|
|
### Cache effectiveness
|
|
|
|
| Case | First render | Cache hit | Speedup |
|
|
| --- | --- | --- | --- |
|
|
| Variable `x` | 2 ms | 0.005 ms | 400x |
|
|
| Summation | 10 ms | 0.008 ms | 1250x |
|
|
| Complex integral | 30 ms | 0.01 ms | 3000x |
|
|
|
|
### Sample document (50 formulas, 15 unique)
|
|
|
|
| Strategy | Total time | Main-thread block | UX |
|
|
| --- | --- | --- | --- |
|
|
| No optimization | 250 ms | 250 ms | :warning: Noticeable hitching. |
|
|
| Worker only | 265 ms | 0 ms | :white_check_mark: Smooth but slower. |
|
|
| Worker + cache | 78 ms | 0 ms | :white_check_mark::white_check_mark: Fast *and* smooth. |
|
|
|
|
## Memory footprint
|
|
|
|
```
|
|
Input formula: ~30 bytes
|
|
HTML output: ~150 bytes
|
|
Expansion ratio: ~5x
|
|
One cache entry: ~180 bytes (with key)
|
|
200 entries: ~36 KB
|
|
```
|
|
|
|
**Conclusion:** memory cost is negligible.
|
|
|
|
## Optimization recipes
|
|
|
|
### 1. Tune cache size
|
|
|
|
```ts
|
|
// inside katexWorkerClient.ts
|
|
const CACHE_MAX = 500 // e.g. bump from 200 to 500 for more unique formulas
|
|
```
|
|
|
|
### 2. Pre-render frequent formulas
|
|
|
|
```ts
|
|
import { setKaTeXCache } from 'markstream-vue/workers/katexWorkerClient'
|
|
|
|
const commonFormulas = ['x', 'y', 'E=mc^2', '\\sum_{i=1}^{n}']
|
|
|
|
for (const formula of commonFormulas) {
|
|
requestIdleCallback(() => {
|
|
renderAndCache(formula)
|
|
})
|
|
}
|
|
```
|
|
|
|
### 3. Use `requestIdleCallback`
|
|
|
|
```ts
|
|
if ('requestIdleCallback' in window) {
|
|
requestIdleCallback(() => {
|
|
renderKaTeXInWorker(formula)
|
|
})
|
|
}
|
|
```
|
|
|
|
## Key takeaways
|
|
|
|
1. Worker overhead is tiny (~1-2 ms).
|
|
2. Cache hit rates >70 % are normal, so caching is the real win.
|
|
3. Worker + cache + fallback is the optimal combo.
|
|
4. Memory costs stay under ~100 KB even with aggressive caching.
|
|
5. Users notice the smoother scrolling much more than the extra kilobytes.
|
|
|
|
### Final recommendation
|
|
|
|
**Keep the existing Worker + cache + fallback architecture.**
|
|
|
|
- :white_check_mark: Great performance (cache removes most work).
|
|
- :white_check_mark: Smooth UX (Worker isolates blocking work).
|
|
- :white_check_mark: Stable (fallback guarantees output).
|
|
- :white_check_mark: Memory friendly.
|
|
- :white_check_mark: Progressive enhancement friendly.
|
|
|
|
Nothing else needs changing -- the current design is already the sweet spot. :tada:
|
|
|
|
## References
|
|
|
|
- [Chrome DevTools Performance guide](https://developer.chrome.com/docs/devtools/performance/)
|
|
- [Web Worker performance considerations](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#performance_considerations)
|
|
- [KaTeX performance tips](https://katex.org/docs/performance.html)
|