|
| 1 | +<!DOCTYPE html> |
| 2 | +<meta charset="UTF-8"> |
| 3 | +<title>Wavefont Text Rendering Benchmark</title> |
| 4 | +<style> |
| 5 | + body { font-family: sans-serif; margin: 2rem; } |
| 6 | + .wavefont { |
| 7 | + font-family: wavefont; |
| 8 | + font-size: 50px; |
| 9 | + line-height: 70px; |
| 10 | + letter-spacing: 1px; |
| 11 | + font-variation-settings: 'wght' 30, 'ROND' 0, 'YALN' 0; |
| 12 | + } |
| 13 | + #results { white-space: pre-wrap; font-family: monospace; margin-top: 1rem; } |
| 14 | + .test-area { width: 80vw; overflow: hidden; visibility: hidden; position: absolute; } |
| 15 | + .opt-speed { text-rendering: optimizeSpeed; } |
| 16 | + .opt-smooth { -webkit-font-smoothing: antialiased; } |
| 17 | + .opt-none { -webkit-font-smoothing: none; } |
| 18 | + .no-ligatures { font-variant-ligatures: none; } |
| 19 | + .will-change { will-change: contents; } |
| 20 | + .contain { contain: content; } |
| 21 | +</style> |
| 22 | +<link rel="stylesheet" href="../node_modules/wavefont/wavefont.css"> |
| 23 | + |
| 24 | +<h3>Wavefont Text Rendering Benchmark</h3> |
| 25 | +<button id="run">Run Benchmark</button> |
| 26 | +<div id="results"></div> |
| 27 | + |
| 28 | +<script type="module"> |
| 29 | +// generate fake waveform string: base chars (U+0100-U+01FF) + combining marks |
| 30 | +function genWaveform(n) { |
| 31 | + let str = '' |
| 32 | + for (let i = 0; i < n; i++) { |
| 33 | + str += String.fromCharCode(0x0100 + (i % 128)) |
| 34 | + str += (i % 3 === 0) ? '\u0301' : '\u0300' |
| 35 | + } |
| 36 | + return str |
| 37 | +} |
| 38 | + |
| 39 | +let log = s => { document.getElementById('results').textContent += s + '\n'; console.log(s) } |
| 40 | + |
| 41 | +async function bench(label, el, text, appendText) { |
| 42 | + // force layout |
| 43 | + el.offsetHeight |
| 44 | + |
| 45 | + // 1. Set full text |
| 46 | + let t0 = performance.now() |
| 47 | + el.textContent = text |
| 48 | + el.offsetHeight // force layout |
| 49 | + let setTime = performance.now() - t0 |
| 50 | + |
| 51 | + // 2. Append chunk |
| 52 | + let t1 = performance.now() |
| 53 | + el.firstChild.appendData(appendText) |
| 54 | + el.offsetHeight // force layout |
| 55 | + let appendTime = performance.now() - t1 |
| 56 | + |
| 57 | + // 3. getClientRects on a range near middle |
| 58 | + let mid = Math.floor(el.firstChild.textContent.length / 2) |
| 59 | + let r = new Range() |
| 60 | + r.setStart(el.firstChild, mid) |
| 61 | + r.setEnd(el.firstChild, mid + 1) |
| 62 | + let t2 = performance.now() |
| 63 | + r.getClientRects() |
| 64 | + let rectsTime = performance.now() - t2 |
| 65 | + |
| 66 | + // cleanup |
| 67 | + el.textContent = '' |
| 68 | + el.offsetHeight |
| 69 | + |
| 70 | + log(`${label.padEnd(35)} set=${setTime.toFixed(0)}ms append=${appendTime.toFixed(0)}ms rects=${rectsTime.toFixed(0)}ms`) |
| 71 | +} |
| 72 | + |
| 73 | +document.getElementById('run').onclick = async () => { |
| 74 | + log('Generating test data...') |
| 75 | + let small = genWaveform(1000) // ~2000 chars |
| 76 | + let medium = genWaveform(5000) // ~10000 chars |
| 77 | + let large = genWaveform(15000) // ~30000 chars |
| 78 | + let chunk = genWaveform(200) // ~400 chars append chunk |
| 79 | + |
| 80 | + let configs = [ |
| 81 | + { label: 'baseline (wavefont)', classes: 'wavefont' }, |
| 82 | + { label: '+ optimizeSpeed', classes: 'wavefont opt-speed' }, |
| 83 | + { label: '+ antialiased', classes: 'wavefont opt-smooth' }, |
| 84 | + { label: '+ no smoothing', classes: 'wavefont opt-none' }, |
| 85 | + { label: '+ no ligatures', classes: 'wavefont no-ligatures' }, |
| 86 | + { label: '+ will-change:contents', classes: 'wavefont will-change' }, |
| 87 | + { label: '+ contain:content', classes: 'wavefont contain' }, |
| 88 | + { label: '+ all optimizations', classes: 'wavefont opt-speed opt-smooth no-ligatures contain' }, |
| 89 | + { label: 'monospace (no wavefont)', classes: '', style: 'font-family:monospace;font-size:14px;line-height:20px' }, |
| 90 | + ] |
| 91 | + |
| 92 | + for (let size of [['1K blocks', small], ['5K blocks', medium], ['15K blocks', large]]) { |
| 93 | + log(`\n=== ${size[0]} (${size[1].length} chars) ===`) |
| 94 | + for (let cfg of configs) { |
| 95 | + let el = document.createElement('div') |
| 96 | + el.className = 'test-area ' + cfg.classes |
| 97 | + if (cfg.style) el.style.cssText += cfg.style |
| 98 | + document.body.appendChild(el) |
| 99 | + await bench(cfg.label, el, size[1], chunk) |
| 100 | + el.remove() |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // contenteditable comparison |
| 105 | + log('\n=== contenteditable vs plain div (5K blocks) ===') |
| 106 | + for (let ce of [false, true]) { |
| 107 | + let el = document.createElement('div') |
| 108 | + el.className = 'test-area wavefont opt-speed contain' |
| 109 | + if (ce) el.contentEditable = 'true' |
| 110 | + document.body.appendChild(el) |
| 111 | + await bench(ce ? 'contenteditable=true' : 'contenteditable=false', el, medium, chunk) |
| 112 | + el.remove() |
| 113 | + } |
| 114 | + |
| 115 | + log('\nDone!') |
| 116 | +} |
| 117 | +</script> |
0 commit comments