Precise DOM morphing
morphing typescript dom
at main 384 lines 9.2 kB view raw
1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Morphlex Benchmark</title> 7 <style> 8 * { 9 margin: 0; 10 padding: 0; 11 box-sizing: border-box; 12 } 13 14 body { 15 font-family: 16 system-ui, 17 -apple-system, 18 sans-serif; 19 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 20 min-height: 100vh; 21 padding: 2rem; 22 color: #1f2937; 23 } 24 25 .container { 26 max-width: 900px; 27 margin: 0 auto; 28 } 29 30 header { 31 text-align: center; 32 color: white; 33 margin-bottom: 2rem; 34 } 35 36 h1 { 37 font-size: 2.5rem; 38 margin-bottom: 0.5rem; 39 text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 40 } 41 42 .subtitle { 43 font-size: 1.1rem; 44 opacity: 0.9; 45 } 46 47 .card { 48 background: white; 49 border-radius: 12px; 50 padding: 2rem; 51 margin-bottom: 1.5rem; 52 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 53 } 54 55 .controls { 56 display: flex; 57 gap: 1rem; 58 flex-wrap: wrap; 59 align-items: center; 60 margin-bottom: 1.5rem; 61 } 62 63 label { 64 font-weight: 600; 65 display: flex; 66 align-items: center; 67 gap: 0.5rem; 68 } 69 70 input[type="number"], 71 select { 72 padding: 0.5rem; 73 border: 2px solid #e5e7eb; 74 border-radius: 6px; 75 font-size: 1rem; 76 } 77 78 input[type="number"] { 79 width: 100px; 80 } 81 82 select { 83 min-width: 200px; 84 } 85 86 button { 87 padding: 0.75rem 2rem; 88 background: #6366f1; 89 color: white; 90 border: none; 91 border-radius: 6px; 92 font-size: 1rem; 93 font-weight: 600; 94 cursor: pointer; 95 transition: all 0.2s; 96 } 97 98 button:hover { 99 background: #4f46e5; 100 transform: translateY(-1px); 101 } 102 103 button:disabled { 104 background: #9ca3af; 105 cursor: not-allowed; 106 transform: none; 107 } 108 109 .results { 110 display: none; 111 } 112 113 .results.visible { 114 display: block; 115 } 116 117 .result-item { 118 padding: 1rem; 119 border-bottom: 1px solid #e5e7eb; 120 } 121 122 .result-item:last-child { 123 border-bottom: none; 124 } 125 126 .result-header { 127 display: flex; 128 justify-content: space-between; 129 align-items: center; 130 margin-bottom: 0.5rem; 131 } 132 133 .result-name { 134 font-weight: 600; 135 font-size: 1.1rem; 136 } 137 138 .result-time { 139 font-size: 1.5rem; 140 font-weight: 700; 141 color: #6366f1; 142 } 143 144 .result-details { 145 display: grid; 146 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 147 gap: 1rem; 148 margin-top: 0.75rem; 149 } 150 151 .detail { 152 display: flex; 153 flex-direction: column; 154 } 155 156 .detail-label { 157 font-size: 0.75rem; 158 color: #6b7280; 159 text-transform: uppercase; 160 letter-spacing: 0.5px; 161 } 162 163 .detail-value { 164 font-weight: 600; 165 font-size: 1rem; 166 } 167 168 .progress { 169 margin-top: 0.75rem; 170 text-align: center; 171 color: #6b7280; 172 font-style: italic; 173 } 174 175 #sandbox { 176 display: none; 177 } 178 </style> 179 </head> 180 <body> 181 <div class="container"> 182 <header> 183 <h1>Morphlex Benchmark</h1> 184 <p class="subtitle">Performance testing for DOM morphing</p> 185 </header> 186 187 <div class="card"> 188 <div class="controls"> 189 <label> 190 Benchmark: 191 <select id="benchmarkSelect"> 192 <option value="all">All Benchmarks</option> 193 </select> 194 </label> 195 <label> 196 Iterations: 197 <input type="number" id="iterations" value="1000" min="100" step="100" /> 198 </label> 199 <button id="runBtn">Run Benchmark</button> 200 </div> 201 <div class="progress" id="progress"></div> 202 </div> 203 204 <div class="card results" id="results"> 205 <h2 style="margin-bottom: 1.5rem">Results</h2> 206 <div id="resultsContainer"></div> 207 </div> 208 </div> 209 210 <div id="sandbox"></div> 211 212 <script type="module"> 213 import { morph } from "../dist/morphlex.js" 214 215 const testCases = [ 216 { 217 name: "Simple Text Change", 218 from: "<div>Hello</div>", 219 to: "<div>World</div>", 220 }, 221 { 222 name: "Attribute Update", 223 from: '<div class="foo" id="test">Content</div>', 224 to: '<div class="bar" id="test">Content</div>', 225 }, 226 { 227 name: "Add Children", 228 from: "<ul><li>One</li></ul>", 229 to: "<ul><li>One</li><li>Two</li><li>Three</li></ul>", 230 }, 231 { 232 name: "Remove Children", 233 from: "<ul><li>One</li><li>Two</li><li>Three</li></ul>", 234 to: "<ul><li>One</li></ul>", 235 }, 236 { 237 name: "Reorder Children", 238 from: '<ul><li id="a">A</li><li id="b">B</li><li id="c">C</li></ul>', 239 to: '<ul><li id="c">C</li><li id="a">A</li><li id="b">B</li></ul>', 240 }, 241 { 242 name: "Deep Nested Update", 243 from: "<div><div><div><span>Deep</span></div></div></div>", 244 to: "<div><div><div><span>Nested</span></div></div></div>", 245 }, 246 { 247 name: "Large List (100 items)", 248 from: "<ul>" + Array.from({ length: 100 }, (_, i) => `<li id="item-${i}">Item ${i}</li>`).join("") + "</ul>", 249 to: "<ul>" + Array.from({ length: 100 }, (_, i) => `<li id="item-${i}">Item ${i * 2}</li>`).join("") + "</ul>", 250 }, 251 { 252 name: "Mixed Operations", 253 from: '<div class="old"><p>Text</p><span id="keep">Keep</span></div>', 254 to: '<div class="new"><span id="keep">Keep</span><p>New Text</p><a href="#">Link</a></div>', 255 }, 256 ] 257 258 const sandbox = document.getElementById("sandbox") 259 const runBtn = document.getElementById("runBtn") 260 const benchmarkSelect = document.getElementById("benchmarkSelect") 261 const iterationsInput = document.getElementById("iterations") 262 const progressDiv = document.getElementById("progress") 263 const resultsDiv = document.getElementById("results") 264 const resultsContainer = document.getElementById("resultsContainer") 265 266 // Populate benchmark select 267 testCases.forEach((testCase, index) => { 268 const option = document.createElement("option") 269 option.value = index 270 option.textContent = testCase.name 271 benchmarkSelect.appendChild(option) 272 }) 273 274 function createElements(html) { 275 const temp = document.createElement("div") 276 temp.innerHTML = html 277 return temp.firstChild 278 } 279 280 async function runBenchmark(testCase, iterations) { 281 const times = [] 282 283 for (let i = 0; i < iterations; i++) { 284 const from = createElements(testCase.from) 285 const to = createElements(testCase.to) 286 sandbox.appendChild(from) 287 288 const start = performance.now() 289 morph(from, to) 290 const end = performance.now() 291 292 times.push(end - start) 293 sandbox.innerHTML = "" 294 295 // Yield to browser occasionally 296 if (i % 100 === 0) { 297 await new Promise((resolve) => setTimeout(resolve, 0)) 298 } 299 } 300 301 times.sort((a, b) => a - b) 302 const total = times.reduce((sum, t) => sum + t, 0) 303 const avg = total / times.length 304 const median = times[Math.floor(times.length / 2)] 305 const min = times[0] 306 const max = times[times.length - 1] 307 const p95 = times[Math.floor(times.length * 0.95)] 308 309 return { total, avg, median, min, max, p95, times: times.length } 310 } 311 312 function displayResults(results) { 313 resultsContainer.innerHTML = results 314 .map( 315 (result) => ` 316 <div class="result-item"> 317 <div class="result-header"> 318 <span class="result-name">${result.name}</span> 319 <span class="result-time">${result.avg.toFixed(3)}ms</span> 320 </div> 321 <div class="result-details"> 322 <div class="detail"> 323 <span class="detail-label">Median</span> 324 <span class="detail-value">${result.median.toFixed(3)}ms</span> 325 </div> 326 <div class="detail"> 327 <span class="detail-label">Min</span> 328 <span class="detail-value">${result.min.toFixed(3)}ms</span> 329 </div> 330 <div class="detail"> 331 <span class="detail-label">Max</span> 332 <span class="detail-value">${result.max.toFixed(3)}ms</span> 333 </div> 334 <div class="detail"> 335 <span class="detail-label">P95</span> 336 <span class="detail-value">${result.p95.toFixed(3)}ms</span> 337 </div> 338 <div class="detail"> 339 <span class="detail-label">Total</span> 340 <span class="detail-value">${result.total.toFixed(1)}ms</span> 341 </div> 342 <div class="detail"> 343 <span class="detail-label">Iterations</span> 344 <span class="detail-value">${result.times}</span> 345 </div> 346 </div> 347 </div> 348 `, 349 ) 350 .join("") 351 352 resultsDiv.classList.add("visible") 353 } 354 355 runBtn.addEventListener("click", async () => { 356 const iterations = parseInt(iterationsInput.value) 357 if (iterations < 1) return 358 359 const selectedValue = benchmarkSelect.value 360 const selectedTests = selectedValue === "all" ? testCases : [testCases[parseInt(selectedValue)]] 361 362 runBtn.disabled = true 363 resultsDiv.classList.remove("visible") 364 progressDiv.textContent = "Running benchmarks..." 365 366 const results = [] 367 368 for (let i = 0; i < selectedTests.length; i++) { 369 const testCase = selectedTests[i] 370 progressDiv.textContent = `Running ${testCase.name} (${i + 1}/${selectedTests.length})...` 371 372 const result = await runBenchmark(testCase, iterations) 373 results.push({ name: testCase.name, ...result }) 374 375 await new Promise((resolve) => setTimeout(resolve, 100)) 376 } 377 378 progressDiv.textContent = "" 379 displayResults(results) 380 runBtn.disabled = false 381 }) 382 </script> 383 </body> 384</html>