Precise DOM morphing
morphing
typescript
dom
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 Suite</title>
7 <style>
8 :root {
9 --primary: #6366f1;
10 --success: #10b981;
11 --warning: #f59e0b;
12 --danger: #ef4444;
13 --dark: #1f2937;
14 --light: #f3f4f6;
15 --white: #ffffff;
16 }
17
18 * {
19 margin: 0;
20 padding: 0;
21 box-sizing: border-box;
22 }
23
24 body {
25 font-family:
26 system-ui,
27 -apple-system,
28 BlinkMacSystemFont,
29 "Segoe UI",
30 Roboto,
31 Oxygen,
32 Ubuntu,
33 sans-serif;
34 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
35 min-height: 100vh;
36 padding: 2rem;
37 }
38
39 .container {
40 max-width: 1200px;
41 margin: 0 auto;
42 }
43
44 header {
45 text-align: center;
46 color: var(--white);
47 margin-bottom: 2rem;
48 }
49
50 h1 {
51 font-size: 3rem;
52 margin-bottom: 0.5rem;
53 text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
54 }
55
56 .subtitle {
57 font-size: 1.2rem;
58 opacity: 0.95;
59 }
60
61 .controls {
62 background: var(--white);
63 border-radius: 12px;
64 padding: 1.5rem;
65 margin-bottom: 2rem;
66 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
67 }
68
69 .control-group {
70 display: flex;
71 gap: 1rem;
72 align-items: center;
73 flex-wrap: wrap;
74 }
75
76 label {
77 font-weight: 600;
78 color: var(--dark);
79 }
80
81 input[type="number"] {
82 padding: 0.5rem;
83 border: 2px solid var(--light);
84 border-radius: 6px;
85 font-size: 1rem;
86 width: 120px;
87 }
88
89 button {
90 padding: 0.75rem 2rem;
91 background: var(--primary);
92 color: var(--white);
93 border: none;
94 border-radius: 6px;
95 font-size: 1rem;
96 font-weight: 600;
97 cursor: pointer;
98 transition:
99 transform 0.2s,
100 box-shadow 0.2s;
101 }
102
103 button:hover:not(:disabled) {
104 transform: translateY(-2px);
105 box-shadow: 0 5px 15px rgba(99, 102, 241, 0.4);
106 }
107
108 button:disabled {
109 background: var(--light);
110 color: #9ca3af;
111 cursor: not-allowed;
112 }
113
114 button.stop {
115 background: var(--danger);
116 }
117
118 .status {
119 margin-left: auto;
120 padding: 0.5rem 1rem;
121 border-radius: 20px;
122 background: var(--light);
123 font-weight: 600;
124 color: var(--dark);
125 }
126
127 .status.running {
128 background: var(--warning);
129 color: var(--white);
130 animation: pulse 1s infinite;
131 }
132
133 .status.complete {
134 background: var(--success);
135 color: var(--white);
136 }
137
138 @keyframes pulse {
139 0%,
140 100% {
141 opacity: 1;
142 }
143 50% {
144 opacity: 0.7;
145 }
146 }
147
148 .results {
149 background: var(--white);
150 border-radius: 12px;
151 padding: 1.5rem;
152 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
153 }
154
155 .test-case {
156 margin-bottom: 2rem;
157 padding: 1.5rem;
158 background: var(--light);
159 border-radius: 8px;
160 }
161
162 .test-case h3 {
163 color: var(--primary);
164 margin-bottom: 0.5rem;
165 }
166
167 .test-description {
168 color: #6b7280;
169 margin-bottom: 1rem;
170 }
171
172 .library-result {
173 display: flex;
174 justify-content: space-between;
175 align-items: center;
176 padding: 0.75rem;
177 margin: 0.5rem 0;
178 background: var(--white);
179 border-radius: 6px;
180 transition: transform 0.2s;
181 }
182
183 .library-result:hover {
184 transform: translateX(5px);
185 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
186 }
187
188 .library-name {
189 font-weight: 600;
190 min-width: 150px;
191 }
192
193 .metrics {
194 display: flex;
195 gap: 2rem;
196 flex: 1;
197 justify-content: center;
198 }
199
200 .metric {
201 text-align: center;
202 }
203
204 .metric-value {
205 font-size: 1.2rem;
206 font-weight: 700;
207 color: var(--primary);
208 }
209
210 .metric-label {
211 font-size: 0.8rem;
212 color: #9ca3af;
213 text-transform: uppercase;
214 letter-spacing: 0.5px;
215 }
216
217 .rank {
218 min-width: 50px;
219 text-align: center;
220 font-weight: 700;
221 }
222
223 .rank-1 {
224 color: #fbbf24;
225 font-size: 1.5rem;
226 }
227
228 .rank-2 {
229 color: #9ca3af;
230 font-size: 1.3rem;
231 }
232
233 .rank-3 {
234 color: #cd7f32;
235 font-size: 1.2rem;
236 }
237
238 .summary {
239 margin-top: 2rem;
240 padding: 2rem;
241 background: linear-gradient(135deg, var(--primary), #8b5cf6);
242 border-radius: 12px;
243 color: var(--white);
244 }
245
246 .summary h2 {
247 margin-bottom: 1.5rem;
248 text-align: center;
249 }
250
251 .summary-grid {
252 display: grid;
253 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
254 gap: 1.5rem;
255 }
256
257 .summary-card {
258 background: rgba(255, 255, 255, 0.15);
259 backdrop-filter: blur(10px);
260 padding: 1.5rem;
261 border-radius: 8px;
262 text-align: center;
263 }
264
265 .summary-library {
266 font-size: 1.2rem;
267 font-weight: 700;
268 margin-bottom: 0.5rem;
269 }
270
271 .summary-score {
272 font-size: 2rem;
273 font-weight: 900;
274 }
275
276 .progress-bar {
277 width: 100%;
278 height: 6px;
279 background: var(--light);
280 border-radius: 3px;
281 overflow: hidden;
282 margin-top: 1rem;
283 }
284
285 .progress-fill {
286 height: 100%;
287 background: var(--success);
288 border-radius: 3px;
289 transition: width 0.3s ease;
290 }
291
292 #sandbox {
293 display: none;
294 }
295
296 .error {
297 background: #fee;
298 border: 2px solid var(--danger);
299 color: var(--danger);
300 padding: 1rem;
301 border-radius: 8px;
302 margin: 1rem 0;
303 }
304
305 .chart-container {
306 margin: 2rem 0;
307 padding: 1.5rem;
308 background: var(--white);
309 border-radius: 8px;
310 }
311
312 .bar-chart {
313 display: flex;
314 flex-direction: column;
315 gap: 0.5rem;
316 }
317
318 .bar-row {
319 display: flex;
320 align-items: center;
321 gap: 1rem;
322 }
323
324 .bar-label {
325 min-width: 120px;
326 font-weight: 600;
327 }
328
329 .bar {
330 height: 30px;
331 background: linear-gradient(90deg, var(--primary), #8b5cf6);
332 border-radius: 4px;
333 display: flex;
334 align-items: center;
335 padding: 0 0.5rem;
336 color: var(--white);
337 font-weight: 600;
338 font-size: 0.9rem;
339 }
340 </style>
341 </head>
342 <body>
343 <div class="container">
344 <header>
345 <h1>⚡ Morphlex Benchmark Suite</h1>
346 <p class="subtitle">Performance comparison of DOM morphing libraries</p>
347 </header>
348
349 <div class="controls">
350 <div class="control-group">
351 <label for="iterations">Iterations:</label>
352 <input type="number" id="iterations" value="5000" min="1000" max="50000" step="1000" />
353
354 <label for="warmup">Warmup:</label>
355 <input type="number" id="warmup" value="500" min="100" max="5000" step="100" />
356
357 <button id="runBtn">🚀 Run Benchmark</button>
358 <button id="stopBtn" class="stop" style="display: none">⏹ Stop</button>
359
360 <div class="status" id="status">Ready</div>
361 </div>
362 <div class="progress-bar">
363 <div class="progress-fill" id="progress" style="width: 0%"></div>
364 </div>
365 </div>
366
367 <div class="results" id="results">
368 <p style="text-align: center; color: #9ca3af; padding: 2rem">Click "Run Benchmark" to start testing...</p>
369 </div>
370 </div>
371
372 <div id="sandbox"></div>
373
374 <script type="module">
375 // Using local morphlex build
376 // To run this benchmark:
377 // 1. Run 'bun run build' to build morphlex
378 // 2. Start a local server: 'bun run --bun vite' or 'python -m http.server' in the morphlex root
379 // 3. Open http://localhost:5173/benchmark/ (or appropriate port)
380 import { morph as morphlex } from "../dist/morphlex.min.js"
381 import { morph as dataMorph } from "../dist/data-morph.min.js"
382 import { Idiomorph } from "https://unpkg.com/idiomorph@0.7.4/dist/idiomorph.esm.js"
383 import morphdom from "https://unpkg.com/morphdom@2.7.7/dist/morphdom-esm.js"
384 // Try loading nanomorph from jsdelivr with ESM
385 import nanomorph from "https://cdn.jsdelivr.net/npm/nanomorph@5.4.3/+esm"
386 // Load Alpine.js and Alpine morph
387 import Alpine from "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/module.esm.js"
388 import Morph from "https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/module.esm.js"
389
390 // Initialize Alpine with morph plugin
391 Alpine.plugin(Morph)
392 window.Alpine = Alpine
393 Alpine.start()
394
395 const sandbox = document.getElementById("sandbox")
396 let isRunning = false
397 let stopRequested = false
398
399 // DOM Operation Counter using MutationObserver
400 class DOMOperationCounter {
401 constructor() {
402 this.reset()
403 }
404
405 reset() {
406 this.counts = {
407 childList: 0,
408 attributes: 0,
409 characterData: 0,
410 addedNodes: 0,
411 removedNodes: 0,
412 total: 0,
413 }
414 this.attributeNames = new Map() // Map of attribute name -> count
415 }
416
417 start(targetNode) {
418 this.reset()
419 this.observer = new MutationObserver((mutations) => {
420 this.processMutations(mutations)
421 })
422
423 this.observer.observe(targetNode, {
424 childList: true,
425 attributes: true,
426 characterData: true,
427 subtree: true,
428 attributeOldValue: false,
429 characterDataOldValue: false,
430 })
431 }
432
433 processMutations(mutations) {
434 for (const mutation of mutations) {
435 if (mutation.type === "childList") {
436 this.counts.childList++
437 this.counts.addedNodes += mutation.addedNodes.length
438 this.counts.removedNodes += mutation.removedNodes.length
439 } else if (mutation.type === "attributes") {
440 this.counts.attributes++
441 // Track the attribute name
442 if (mutation.attributeName) {
443 const count = this.attributeNames.get(mutation.attributeName) || 0
444 this.attributeNames.set(mutation.attributeName, count + 1)
445 }
446 } else if (mutation.type === "characterData") {
447 this.counts.characterData++
448 }
449 this.counts.total++
450 }
451 }
452
453 stop() {
454 if (this.observer) {
455 // Process any pending mutations that haven't been delivered yet
456 const pendingMutations = this.observer.takeRecords()
457 this.processMutations(pendingMutations)
458 this.observer.disconnect()
459 this.observer = null
460 }
461 return {
462 ...this.counts,
463 attributeNames: Array.from(this.attributeNames.entries()).sort((a, b) => b[1] - a[1]),
464 }
465 }
466
467 getCounts() {
468 return { ...this.counts }
469 }
470 }
471
472 class BrowserBenchmark {
473 constructor(iterations = 1000, warmupIterations = 100) {
474 this.iterations = iterations
475 this.warmupIterations = warmupIterations
476 this.results = []
477 }
478
479 async runSingleBenchmark(library, testCase, morphFn) {
480 // Warmup
481 for (let i = 0; i < this.warmupIterations; i++) {
482 const { from, to } = testCase.setup()
483 sandbox.appendChild(from)
484 morphFn(from, to)
485 sandbox.innerHTML = ""
486 }
487
488 // Allow browser to settle
489 await new Promise((resolve) => setTimeout(resolve, 10))
490
491 // First, do a quick test to see how fast the operation is
492 const testRun = testCase.setup()
493 sandbox.appendChild(testRun.from)
494 const testStart = performance.now()
495 morphFn(testRun.from, testRun.to)
496 const testEnd = performance.now()
497 sandbox.innerHTML = ""
498 const singleOpTime = testEnd - testStart
499
500 // If operation is very fast (< 0.1ms), batch multiple operations per measurement
501 const batchSize = singleOpTime < 0.1 ? Math.min(100, Math.ceil(0.1 / Math.max(singleOpTime, 0.001))) : 1
502
503 // Actual benchmark - use higher precision timing with batching
504 const times = []
505 const actualIterations = Math.ceil(this.iterations / batchSize)
506
507 // Measure DOM operations for a single operation (not batched)
508 const domCounter = new DOMOperationCounter()
509 const domTestSetup = testCase.setup()
510 sandbox.appendChild(domTestSetup.from)
511 domCounter.start(domTestSetup.from)
512 morphFn(domTestSetup.from, domTestSetup.to)
513 const domOperations = domCounter.stop()
514 sandbox.innerHTML = ""
515
516 for (let i = 0; i < actualIterations; i++) {
517 // Prepare batch
518 const batch = []
519 for (let j = 0; j < batchSize; j++) {
520 batch.push(testCase.setup())
521 }
522
523 // Measure batch execution
524 const start = performance.now()
525 for (let j = 0; j < batchSize; j++) {
526 sandbox.appendChild(batch[j].from)
527 morphFn(batch[j].from, batch[j].to)
528 sandbox.innerHTML = ""
529 }
530 const end = performance.now()
531
532 const elapsed = (end - start) / batchSize // Average per operation
533 times.push(elapsed)
534 }
535
536 // Calculate statistics with better precision
537 const totalTime = times.reduce((a, b) => a + b, 0)
538 const averageTime = totalTime / times.length
539 const sorted = [...times].sort((a, b) => a - b)
540 const median = sorted[Math.floor(sorted.length / 2)]
541 const min = sorted[0]
542 const max = sorted[sorted.length - 1]
543 // Prevent infinity and provide more precision
544 const opsPerSecond = averageTime > 0 ? 1000 / averageTime : 0
545
546 return {
547 library,
548 testName: testCase.name,
549 iterations: actualIterations * batchSize,
550 totalTime: totalTime,
551 averageTime,
552 median,
553 min,
554 max,
555 opsPerSecond,
556 domOperations,
557 }
558 }
559
560 async runTestCase(testCase, onProgress) {
561 const results = []
562 const libraries = [
563 { name: "morphlex", fn: (from, to) => morphlex(from, to) },
564 { name: "data-morph", fn: (from, to) => dataMorph(from, to.cloneNode(true), "outer") },
565 { name: "idiomorph", fn: (from, to) => Idiomorph.morph(from, to) },
566 { name: "morphdom", fn: (from, to) => morphdom(from, to.cloneNode(true)) },
567 { name: "nanomorph", fn: (from, to) => nanomorph(from, to.cloneNode(true)) },
568 { name: "alpine-morph", fn: (from, to) => Alpine.morph(from, to) },
569 ]
570
571 for (const lib of libraries) {
572 if (stopRequested) break
573
574 try {
575 const result = await this.runSingleBenchmark(lib.name, testCase, lib.fn)
576 results.push(result)
577 this.results.push(result)
578 if (onProgress) onProgress(result)
579 } catch (error) {
580 console.error(`Error running ${lib.name}:`, error)
581 results.push({
582 library: lib.name,
583 testName: testCase.name,
584 error: error.message,
585 })
586 }
587
588 // Small delay between libraries
589 await new Promise((resolve) => setTimeout(resolve, 50))
590 }
591
592 return results
593 }
594 }
595
596 // Test cases
597 const testCases = [
598 {
599 name: "Simple Text Update",
600 description: "Morphing a single text node change",
601 setup: () => {
602 const from = document.createElement("div")
603 from.innerHTML = "<p>Hello World</p>"
604 const to = document.createElement("div")
605 to.innerHTML = "<p>Hello Morphlex</p>"
606 return { from: from.firstElementChild, to: to.firstElementChild }
607 },
608 },
609 {
610 name: "Attribute Changes",
611 description: "Updating multiple attributes on elements",
612 setup: () => {
613 const from = document.createElement("div")
614 from.innerHTML = `
615 <div class="old-class" data-value="1">
616 <span id="test" title="old">Content</span>
617 </div>
618 `
619 const to = document.createElement("div")
620 to.innerHTML = `
621 <div class="new-class" data-value="2" data-new="true">
622 <span id="test" title="new" aria-label="label">Content</span>
623 </div>
624 `
625 return { from: from.firstElementChild, to: to.firstElementChild }
626 },
627 },
628 {
629 name: "List Reordering",
630 description: "Reordering items in a list",
631 setup: () => {
632 const from = document.createElement("ul")
633 from.innerHTML = `
634 <li>First</li>
635 <li>Second</li>
636 <li>Third</li>
637 <li>Fourth</li>
638 <li>Fifth</li>
639 `
640 const to = document.createElement("ul")
641 to.innerHTML = `
642 <li>Third</li>
643 <li>First</li>
644 <li>Fifth</li>
645 <li>Second</li>
646 <li>Fourth</li>
647 `
648 return { from, to }
649 },
650 },
651 {
652 name: "Large List",
653 description: "Morphing a list with 50 items",
654 setup: () => {
655 const from = document.createElement("ul")
656 for (let i = 1; i <= 50; i++) {
657 const li = document.createElement("li")
658 li.textContent = `Item ${i}`
659 from.appendChild(li)
660 }
661 const to = document.createElement("ul")
662 const indices = Array.from({ length: 50 }, (_, i) => i + 1)
663 // Simple shuffle
664 for (let i = indices.length - 1; i > 0; i--) {
665 const j = Math.floor(Math.random() * (i + 1))
666 ;[indices[i], indices[j]] = [indices[j], indices[i]]
667 }
668 for (const i of indices.slice(0, 45)) {
669 const li = document.createElement("li")
670 li.textContent = i % 3 === 0 ? `Modified Item ${i}` : `Item ${i}`
671 to.appendChild(li)
672 }
673 return { from, to }
674 },
675 },
676 {
677 name: "Large List - Add One Item",
678 description: "Adding a single item to a list of 100 items",
679 setup: () => {
680 const from = document.createElement("ul")
681 for (let i = 1; i <= 100; i++) {
682 const li = document.createElement("li")
683 li.textContent = `Item ${i}`
684 from.appendChild(li)
685 }
686 const to = document.createElement("ul")
687 for (let i = 1; i <= 100; i++) {
688 const li = document.createElement("li")
689 li.textContent = `Item ${i}`
690 to.appendChild(li)
691 }
692 // Add new item at position 50
693 const newLi = document.createElement("li")
694 newLi.textContent = "New Item"
695 to.insertBefore(newLi, to.children[50])
696 return { from, to }
697 },
698 },
699 {
700 name: "Large List - Remove One Item",
701 description: "Removing a single item from a list of 100 items",
702 setup: () => {
703 const from = document.createElement("ul")
704 for (let i = 1; i <= 100; i++) {
705 const li = document.createElement("li")
706 li.textContent = `Item ${i}`
707 from.appendChild(li)
708 }
709 const to = document.createElement("ul")
710 for (let i = 1; i <= 100; i++) {
711 if (i === 50) continue // Skip item 50
712 const li = document.createElement("li")
713 li.textContent = `Item ${i}`
714 to.appendChild(li)
715 }
716 return { from, to }
717 },
718 },
719 {
720 name: "Large List - Resort All Items",
721 description: "Resorting all items in a list of 100 items",
722 setup: () => {
723 const from = document.createElement("ul")
724 for (let i = 1; i <= 100; i++) {
725 const li = document.createElement("li")
726 li.textContent = `Item ${i}`
727 from.appendChild(li)
728 }
729 const to = document.createElement("ul")
730 // Reverse the order
731 for (let i = 100; i >= 1; i--) {
732 const li = document.createElement("li")
733 li.textContent = `Item ${i}`
734 to.appendChild(li)
735 }
736 return { from, to }
737 },
738 },
739 {
740 name: "Large List - Partial Reorder",
741 description: "Reordering some items in a list of 100 items while keeping many in place",
742 setup: () => {
743 const from = document.createElement("ul")
744 for (let i = 1; i <= 100; i++) {
745 const li = document.createElement("li")
746 li.id = `item-${i}`
747 li.textContent = `Item ${i}`
748 from.appendChild(li)
749 }
750 const to = document.createElement("ul")
751 // Partial reorder: move every 5th item to a different position
752 // [1,2,3,4,5,6,7,8,9,10,...] → [5,1,2,3,4,10,6,7,8,9,15,11,12,13,14,20,...]
753 // This keeps most items in order (good for LIS) while shuffling some
754 const items = []
755 for (let i = 1; i <= 100; i++) {
756 items.push(i)
757 }
758 const reordered = []
759 for (let i = 0; i < items.length; i += 5) {
760 if (i + 4 < items.length) {
761 // Move 5th item to front of group
762 reordered.push(items[i + 4])
763 reordered.push(items[i])
764 reordered.push(items[i + 1])
765 reordered.push(items[i + 2])
766 reordered.push(items[i + 3])
767 } else {
768 // Handle remaining items
769 for (let j = i; j < items.length; j++) {
770 reordered.push(items[j])
771 }
772 }
773 }
774 for (const num of reordered) {
775 const li = document.createElement("li")
776 li.id = `item-${num}`
777 li.textContent = `Item ${num}`
778 to.appendChild(li)
779 }
780 return { from, to }
781 },
782 },
783 {
784 name: "Deep Nesting",
785 description: "Morphing deeply nested structures",
786 setup: () => {
787 const from = document.createElement("div")
788 from.innerHTML = `
789 <div>
790 <section>
791 <article>
792 <header>
793 <h1>Title</h1>
794 <p>Subtitle</p>
795 </header>
796 <div>
797 <p>Paragraph 1</p>
798 <p>Paragraph 2</p>
799 </div>
800 </article>
801 </section>
802 </div>
803 `
804 const to = document.createElement("div")
805 to.innerHTML = `
806 <div>
807 <section>
808 <article>
809 <header>
810 <h1>New Title</h1>
811 <p>New Subtitle</p>
812 </header>
813 <div>
814 <p>Modified Paragraph 1</p>
815 <p>Paragraph 2</p>
816 <p>Paragraph 3</p>
817 </div>
818 </article>
819 </section>
820 </div>
821 `
822 return { from: from.firstElementChild, to: to.firstElementChild }
823 },
824 },
825 ]
826
827 // UI Functions
828 function updateProgress(current, total) {
829 const percent = (current / total) * 100
830 document.getElementById("progress").style.width = `${percent}%`
831 }
832
833 function displayResults(allResults) {
834 const resultsEl = document.getElementById("results")
835 resultsEl.innerHTML = ""
836
837 // Group results by test case
838 const testGroups = {}
839 allResults.forEach((result) => {
840 if (!testGroups[result.testName]) {
841 testGroups[result.testName] = []
842 }
843 testGroups[result.testName].push(result)
844 })
845
846 // Calculate overall stats first for summary
847 const libraryStats = {}
848 allResults.forEach((result) => {
849 if (!result.error) {
850 if (!libraryStats[result.library]) {
851 libraryStats[result.library] = {
852 totalTime: 0,
853 count: 0,
854 wins: 0,
855 }
856 }
857 libraryStats[result.library].totalTime += result.averageTime
858 libraryStats[result.library].count++
859 }
860 })
861
862 // Count wins
863 Object.keys(testGroups).forEach((testName) => {
864 const testResults = allResults.filter((r) => r.testName === testName && !r.error)
865 if (testResults.length > 0) {
866 const winner = testResults.reduce((min, r) => (r.averageTime < min.averageTime ? r : min))
867 if (libraryStats[winner.library]) {
868 libraryStats[winner.library].wins++
869 }
870 }
871 })
872
873 const summaryData = Object.entries(libraryStats)
874 .map(([library, stats]) => ({
875 library,
876 avgTime: stats.totalTime / stats.count,
877 wins: stats.wins,
878 totalTests: stats.count,
879 }))
880 .sort((a, b) => a.avgTime - b.avgTime)
881
882 // Create summary section
883 const summary = document.createElement("div")
884 summary.className = "summary"
885 summary.innerHTML = "<h2>🏆 Overall Results</h2>"
886
887 const summaryGrid = document.createElement("div")
888 summaryGrid.className = "summary-grid"
889
890 summaryData.forEach((data, index) => {
891 const card = document.createElement("div")
892 card.className = "summary-card"
893 const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `#${index + 1}`
894 const avgTime = data.avgTime < 0.001 ? data.avgTime.toExponential(2) : data.avgTime.toFixed(4)
895
896 card.innerHTML = `
897 <div style="font-size: 2rem; margin-bottom: 0.5rem">${medal}</div>
898 <div class="summary-library">${data.library}</div>
899 <div class="summary-score">${avgTime} ms</div>
900 <div style="margin-top: 0.5rem; font-size: 0.9rem">
901 ${data.wins} wins / ${data.totalTests} tests
902 </div>
903 `
904 summaryGrid.appendChild(card)
905 })
906
907 summary.appendChild(summaryGrid)
908 resultsEl.appendChild(summary)
909
910 // Display each test case
911 Object.entries(testGroups).forEach(([testName, results]) => {
912 const testCase = document.createElement("div")
913 testCase.className = "test-case"
914
915 const testDesc = testCases.find((t) => t.name === testName)
916 testCase.innerHTML = `
917 <h3>${testName}</h3>
918 <p class="test-description">${testDesc ? testDesc.description : ""}</p>
919 `
920
921 // Sort by performance
922 const sorted = [...results].sort((a, b) => {
923 if (a.error) return 1
924 if (b.error) return -1
925 return a.averageTime - b.averageTime
926 })
927
928 // Create visual results for each library
929 sorted.forEach((result, index) => {
930 const libResult = document.createElement("div")
931 libResult.className = "library-result"
932
933 if (result.error) {
934 libResult.innerHTML = `
935 <div class="library-name">${result.library}</div>
936 <div class="error">ERROR: ${result.error}</div>
937 `
938 } else {
939 const msOp = result.averageTime < 0.001 ? result.averageTime.toExponential(2) : result.averageTime.toFixed(4)
940 const opsPerSec =
941 result.opsPerSecond > 1000000
942 ? result.opsPerSecond.toExponential(2)
943 : Math.round(result.opsPerSecond).toLocaleString()
944
945 const dom = result.domOperations
946 const rankBadge = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : ""
947
948 libResult.innerHTML = `
949 <div class="library-name">${result.library}</div>
950 <div class="metrics">
951 <div class="metric">
952 <div class="metric-value">${msOp}</div>
953 <div class="metric-label">ms/op</div>
954 </div>
955 <div class="metric">
956 <div class="metric-value">${opsPerSec}</div>
957 <div class="metric-label">ops/sec</div>
958 </div>
959 <div class="metric">
960 <div class="metric-value">${dom ? dom.total : "N/A"}</div>
961 <div class="metric-label">DOM ops</div>
962 </div>
963 <div class="metric">
964 <div class="metric-value">${dom ? dom.addedNodes + dom.removedNodes : "N/A"}</div>
965 <div class="metric-label">nodes +/-</div>
966 </div>
967 <div class="metric">
968 <div class="metric-value">${dom ? dom.attributes : "N/A"}</div>
969 <div class="metric-label">attrs</div>
970 </div>
971 </div>
972 <div class="rank rank-${index + 1}">${rankBadge}</div>
973 `
974 }
975
976 testCase.appendChild(libResult)
977 })
978
979 resultsEl.appendChild(testCase)
980 })
981 }
982
983 // Event handlers
984 document.getElementById("runBtn").addEventListener("click", async () => {
985 if (isRunning) return
986
987 isRunning = true
988 stopRequested = false
989 const runBtn = document.getElementById("runBtn")
990 const stopBtn = document.getElementById("stopBtn")
991 const statusEl = document.getElementById("status")
992 const resultsEl = document.getElementById("results")
993
994 runBtn.style.display = "none"
995 stopBtn.style.display = "inline-block"
996 statusEl.textContent = "Running..."
997 statusEl.className = "status running"
998 resultsEl.innerHTML = ""
999
1000 const iterations = parseInt(document.getElementById("iterations").value)
1001 const warmup = parseInt(document.getElementById("warmup").value)
1002
1003 const benchmark = new BrowserBenchmark(iterations, warmup)
1004 const totalTests = testCases.length * 4 // 4 libraries per test
1005 let completedTests = 0
1006
1007 for (const testCase of testCases) {
1008 if (stopRequested) break
1009
1010 await benchmark.runTestCase(testCase, (result) => {
1011 completedTests++
1012 updateProgress(completedTests, totalTests)
1013 })
1014 }
1015
1016 if (!stopRequested && benchmark.results.length > 0) {
1017 displayResults(benchmark.results)
1018 }
1019
1020 isRunning = false
1021 runBtn.style.display = "inline-block"
1022 stopBtn.style.display = "none"
1023 statusEl.textContent = stopRequested ? "Stopped" : "Complete"
1024 statusEl.className = stopRequested ? "status" : "status complete"
1025 updateProgress(100, 100)
1026 })
1027
1028 document.getElementById("stopBtn").addEventListener("click", () => {
1029 stopRequested = true
1030 })
1031 </script>
1032 </body>
1033</html>