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 Heatmap Profiling</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, #0f172a 0%, #1e293b 60%, #334155 100%);
20 min-height: 100vh;
21 padding: 2rem;
22 color: #e2e8f0;
23 }
24
25 .container {
26 max-width: 900px;
27 margin: 0 auto;
28 }
29
30 h1 {
31 font-size: 2rem;
32 margin-bottom: 0.5rem;
33 }
34
35 .subtitle {
36 color: #cbd5e1;
37 margin-bottom: 1.5rem;
38 }
39
40 .card {
41 background: rgba(15, 23, 42, 0.65);
42 border: 1px solid rgba(148, 163, 184, 0.25);
43 border-radius: 12px;
44 padding: 1.25rem;
45 margin-bottom: 1rem;
46 }
47
48 .controls {
49 display: flex;
50 gap: 0.75rem;
51 flex-wrap: wrap;
52 align-items: center;
53 }
54
55 label {
56 display: flex;
57 gap: 0.5rem;
58 align-items: center;
59 font-weight: 600;
60 }
61
62 select,
63 input,
64 button {
65 padding: 0.5rem 0.75rem;
66 border-radius: 8px;
67 border: 1px solid #64748b;
68 background: #0f172a;
69 color: #e2e8f0;
70 }
71
72 button {
73 cursor: pointer;
74 font-weight: 600;
75 background: #0369a1;
76 border-color: #0284c7;
77 }
78
79 button:disabled {
80 opacity: 0.6;
81 cursor: not-allowed;
82 }
83
84 pre {
85 white-space: pre-wrap;
86 font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
87 font-size: 0.85rem;
88 line-height: 1.5;
89 color: #93c5fd;
90 }
91
92 #sandbox {
93 display: none;
94 }
95 </style>
96 </head>
97 <body>
98 <div class="container">
99 <h1>Morphlex Heatmap Profiling</h1>
100 <p class="subtitle">
101 Use this page with Chrome/Edge DevTools Performance to get a flame chart (heatmap) for known hotspot scenarios.
102 </p>
103
104 <div class="card">
105 <div class="controls">
106 <label>
107 Scenario
108 <select id="scenario"></select>
109 </label>
110 <label>
111 Iterations
112 <input id="iterations" type="number" value="300" min="10" step="10" />
113 </label>
114 <button id="run">Run Profile Workload</button>
115 </div>
116 </div>
117
118 <div class="card">
119 <pre id="status">Open DevTools -> Performance, press Record, then click "Run Profile Workload".</pre>
120 </div>
121 </div>
122
123 <div id="sandbox"></div>
124
125 <script type="module">
126 import { morph } from "../dist/morphlex.js"
127
128 function buildIdRelatedCards(count, reverseOrder, updatedText) {
129 const indices = Array.from({ length: count }, (_, i) => i)
130 if (reverseOrder) indices.reverse()
131 return indices
132 .map((i) => {
133 const next = (i + 1) % count
134 return `<article data-card="${i}"><h3 id="card-${i}">Card ${i}</h3><a href="#card-${next}" name="card-link-${i}">Next</a><p>${updatedText ? `Card ${i} updated` : `Card ${i}`}</p></article>`
135 })
136 .join("")
137 }
138
139 function buildDeepNestedIdTrees(count, depth, reverseOrder, updatedText) {
140 const indices = Array.from({ length: count }, (_, i) => i)
141 if (reverseOrder) indices.reverse()
142 return indices
143 .map((i) => {
144 const next = (i + 1) % count
145 let nested = `<span id="deep-id-${i}">Node ${i}${updatedText ? " updated" : ""}</span><a href="#deep-id-${next}">Next</a>`
146 for (let d = 0; d < depth; d++) {
147 nested = `<div data-depth="${d}" data-key="${i}-${d}">${nested}</div>`
148 }
149 return `<article data-chain="${i}">${nested}</article>`
150 })
151 .join("")
152 }
153
154 const scenarios = [
155 {
156 name: "idset-matching-related-cards-60",
157 from: `<section>${buildIdRelatedCards(60, false, false)}</section>`,
158 to: `<section>${buildIdRelatedCards(60, true, true)}</section>`,
159 },
160 {
161 name: "deep-id-ancestry-40x8",
162 from: `<section>${buildDeepNestedIdTrees(40, 8, false, false)}</section>`,
163 to: `<section>${buildDeepNestedIdTrees(40, 8, true, true)}</section>`,
164 },
165 {
166 name: "dirty-form-text-inputs-60",
167 from: `<form>${Array.from({ length: 60 }, (_, i) => `<input name="field-${i}" value="value-${i}">`).join("")}</form>`,
168 to: `<form>${Array.from({ length: 60 }, (_, i) => `<input name="field-${i}" value="new-${i}" data-next="1">`).join("")}</form>`,
169 setup: (from) => {
170 const textInputs = from.querySelectorAll("input[name^='field-']")
171 for (let i = 0; i < textInputs.length; i++) {
172 const input = textInputs[i]
173 input.value = `${input.value}-dirty`
174 }
175 },
176 },
177 ]
178
179 const scenarioSelect = document.getElementById("scenario")
180 const iterationsInput = document.getElementById("iterations")
181 const runButton = document.getElementById("run")
182 const status = document.getElementById("status")
183 const sandbox = document.getElementById("sandbox")
184
185 for (let i = 0; i < scenarios.length; i++) {
186 const option = document.createElement("option")
187 option.value = String(i)
188 option.textContent = scenarios[i].name
189 scenarioSelect.appendChild(option)
190 }
191
192 function createElement(html) {
193 const template = document.createElement("template")
194 template.innerHTML = html
195 return template.content.firstChild
196 }
197
198 async function runScenario(scenario, iterations) {
199 const workload = new Array(iterations)
200 sandbox.textContent = ""
201
202 for (let i = 0; i < iterations; i++) {
203 const from = createElement(scenario.from)
204 const to = createElement(scenario.to)
205 scenario.setup?.(from)
206 sandbox.appendChild(from)
207 workload[i] = { from, to }
208 }
209
210 performance.clearMarks()
211 performance.clearMeasures()
212 performance.mark("morphlex-profile-start")
213
214 for (let i = 0; i < iterations; i++) {
215 const entry = workload[i]
216 morph(entry.from, entry.to)
217
218 if (i % 50 === 0) {
219 await new Promise((resolve) => setTimeout(resolve, 0))
220 }
221 }
222
223 performance.mark("morphlex-profile-end")
224 performance.measure("morphlex-profile-workload", "morphlex-profile-start", "morphlex-profile-end")
225 return performance.getEntriesByName("morphlex-profile-workload")[0]?.duration ?? 0
226 }
227
228 runButton.addEventListener("click", async () => {
229 const scenarioIndex = Number(scenarioSelect.value)
230 const iterations = Number(iterationsInput.value)
231 if (!Number.isFinite(iterations) || iterations <= 0) return
232
233 const scenario = scenarios[scenarioIndex]
234 runButton.disabled = true
235 status.textContent = `Running ${scenario.name}...`
236
237 const totalDuration = await runScenario(scenario, iterations)
238
239 status.textContent = `${scenario.name}\niterations: ${iterations}\nworkload duration: ${totalDuration.toFixed(2)}ms\n\nPrebuild and setup run before morph marks. In DevTools, select the range between morphlex-profile-start and morphlex-profile-end, then filter for \"visitChildNodes\", \"forEachDescendantElementWithId\", and \"isEqualNode\".`
240 runButton.disabled = false
241 })
242 </script>
243 </body>
244</html>