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</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>