Precise DOM morphing
morphing typescript dom
at main 244 lines 6.9 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 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>