Performance
How to profile, benchmark, and optimize Ingglish.
Profiling Scripts
Core Library (packages/core/scripts/profile/)
| Script | Purpose |
|---|---|
benchmark.ts |
Full benchmark suite (1000 iterations, statistics) |
overview.ts |
Quick translation profile |
translate.ts |
translateSync performance |
convert.ts |
Phoneme conversion performance |
cpu-profile.ts |
V8 CPU profiler for flame graphs |
harness.ts |
Shared benchmark and formatting helpers |
DOM Library (packages/dom/scripts/)
| Script | Purpose |
|---|---|
profile-wikipedia.ts |
Profiles a real Wikipedia page (~220KB of HTML) |
profile-tree-walker.ts |
Compares TreeWalker alternatives |
profile-process-node.ts |
Text node processing cost |
profile-dom.ts |
General DOM translation profile |
profile-real-html.ts |
Profiles article-style HTML |
profile-tooltips.ts |
Compares tooltip overhead |
Running Benchmarks
Core Library
cd packages/core
# Full benchmark suite
npx vite-node --script scripts/profile/benchmark.ts
# Quick profile
npx vite-node --script scripts/profile/overview.ts
Sample output:
=== Ingglish Core Benchmarks ===
Iterations: 1000, Warmup: 100
--- Forward Translation ---
translateSync(short text) 0.005ms (min: 0.003ms, max: 0.033ms) 212690 ops/sec
translateSync(medium text) 0.013ms (min: 0.009ms, max: 0.077ms) 74524 ops/sec
--- Reverse Translation ---
reverseTranslateWord(single) 0.005ms (min: 0.003ms, max: 0.544ms) 183851 ops/sec
DOM Library
cd packages/dom
# Profile with real Wikipedia content
npx vite-node --script scripts/profile-wikipedia.ts
# Compare TreeWalker implementations
npx vite-node --script scripts/profile-tree-walker.ts
Sample output from profile-wikipedia.ts (abridged):
=== Wikipedia DOM Profile ===
HTML size: 219.3 KB
--- DOM Statistics ---
Total elements: 2213
Text nodes: 808
Total word occurrences: 1769
Unique words: 927
collectTextNodes: avg: 18.70ms (23.14µs per node)
extractWordsFromNodes: avg: 1.11ms (927 unique words)
applyTranslationsMap: avg: 62.14ms min: 41.51ms max: 114.88ms
=== Summary ===
Total translation time: 62.14ms for 808 text nodes (1769 words)
Breakdown:
- collectTextNodes: 18.70ms (30.1%)
- Text processing: 2.63ms (4.2%)
- DOM updates: 40.82ms (65.7%)
The phases overlap: applyTranslationsMap walks the DOM itself, so its time already includes collecting the text nodes. The total is the applyTranslationsMap time, and the breakdown splits it into its parts. Timings vary from run to run.
Performance Characteristics
Summary
| Path | Complexity | Notes |
|---|---|---|
| Forward (dictionary hit) | O(p) | p = phoneme count |
| Forward (unknown word) | O(n) | n = word length |
| Reverse | O(n) | Pre-sorted at build time |
| Full text | O(w Ă— n) | w = word count |
Every path is linear: nothing is quadratic or exponential.
Forward Translation (translateWord)
| Operation | Complexity | Notes |
|---|---|---|
| Dictionary lookup | O(1) | Hash table, phonemes pre-split at build time |
| ARPAbet→Ingglish | O(p) | p = phoneme count, single pass |
| CamelCase split | O(n) | n = word length, single pass |
| Case detection | O(n) | Single pass through word |
Fallback chain for unknown words
| Strategy | Complexity | Notes |
|---|---|---|
| Custom pronunciations | O(1) | Hash table lookup |
| Initialism check | O(n) | Hash table lookup, then spells out each letter |
| Compound splitting | O(n) | n-2 split points Ă— O(1) lookup each |
| Stemming | O(1) | ~20 suffixes Ă— ~4 variants = constant |
| G2P (grapheme-to-phoneme) rules | O(n) | n chars Ă— ~40 rules (constant) |
Reverse Translation (reverseTranslateWord)
| Operation | Complexity | Notes |
|---|---|---|
| Ingglish→ARPAbet | O(n) | n = word length |
| Phoneme key lookup | O(1) | Hash table, words pre-sorted by frequency at build time |
Infrastructure
| Operation | Complexity | Notes |
|---|---|---|
| Forward dictionary load | O(n) | ~1MB gzipped, phonemes pre-split |
| Reverse dictionary load | O(n) | ~300KB gzipped, words pre-sorted |
| DOM traversal | O(n) | TreeWalker, n = nodes |
Optimization Guidelines
Profile first: measure before optimizing, so you fix the real bottleneck
Reuse collected nodes: pass
textNodestoapplyTranslationsMap()so the DOM isn't walked twiceBatch translations: use
translateWordsInBatches()for large sets of wordsRender in chunks: use
requestAnimationFrameto keep large pages smoothCache translations: the Chrome extension's background worker caches 50K translations
Bundle Splitting
The @ingglish/dictionary package splits its code with dynamic imports:
ingglishindex: the minimal public API (~2KB)- Forward dictionary: loaded on the first
translate()call (~1MB gzipped) - Reverse dictionary: loaded on the first reverse translation (~300KB gzipped)
- Word frequencies: loaded on the first reverse translation (~500KB)
@ingglish/dictionary pre-processes the dictionaries at build time:
- Phonemes pre-split into arrays (no runtime string splitting)
- Reverse dictionary pre-sorted by word frequency (no runtime sorting)
The first page load stays fast, and the heavy data loads only when it's needed.