feat(desktop): embed BoosAPI Desktop client as a subproject
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.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user