Precise DOM morphing
morphing typescript dom

Performance improvements

+28 -19
+28 -19
src/morphlex.ts
··· 1 + const SupportsMoveBefore = "moveBefore" in Element.prototype 1 2 const ParentNodeTypes = new Set([1, 9, 11]) 2 3 const DisablableElements = new Set(["input", "button", "select", "textarea", "option", "optgroup", "fieldset"]) 3 4 const ValuableElements = new Set(["input", "select", "textarea"]) ··· 45 46 afterChildrenVisited?: (parent: ParentNode) => void 46 47 } 47 48 49 + type NodeWithMoveBefore = ParentNode & { 50 + moveBefore: (node: ChildNode, before: ChildNode | null) => void 51 + } 52 + 48 53 export function morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode> | string, options: Options = {}): void { 49 54 if (typeof to === "string") to = parseString(to).childNodes 50 55 new Morph(options).morph(from, to) ··· 78 83 79 84 function moveBefore(parent: ParentNode, node: ChildNode, insertionPoint: ChildNode | null): void { 80 85 if (node === insertionPoint) return 81 - if (node.parentNode === parent && node.nextSibling === insertionPoint) return 82 - 83 - // Use moveBefore when available and the node is already in the same parent 84 - if ("moveBefore" in parent && typeof parent.moveBefore === "function" && node.parentNode === parent) { 85 - parent.moveBefore(node, insertionPoint) 86 - } else { 87 - parent.insertBefore(node, insertionPoint) 86 + if (node.parentNode === parent) { 87 + if (node.nextSibling === insertionPoint) return 88 + if (supportsMoveBefore(parent)) { 89 + parent.moveBefore(node, insertionPoint) 90 + return 91 + } 88 92 } 93 + 94 + parent.insertBefore(node, insertionPoint) 89 95 } 90 96 91 97 class Morph { ··· 134 140 135 141 private morphOneToOne(from: ChildNode, to: ChildNode): void { 136 142 // Fast path: if nodes are exactly the same object, skip morphing 137 - if (from.isSameNode?.(to)) return 143 + if (from === to) return 138 144 if (from.isEqualNode?.(to)) return 139 145 140 146 if (!(this.options.beforeNodeVisited?.(from, to) ?? true)) return ··· 298 304 if (!isElement(node)) continue 299 305 const idSet = this.idMap.get(node) 300 306 if (!idSet) continue 301 - const idSetArray = [...idSet] 302 307 303 - for (const candidate of candidates) { 308 + candidateLoop: for (const candidate of candidates) { 304 309 if (isElement(candidate)) { 305 310 const candidateIdSet = this.idMap.get(candidate) 306 - if (candidateIdSet && idSetArray.some((id) => candidateIdSet.has(id))) { 307 - matches.set(node, candidate) 308 - unmatched.delete(node) 309 - candidates.delete(candidate) 310 - break 311 + if (candidateIdSet) { 312 + for (const id of idSet) { 313 + if (candidateIdSet.has(id)) { 314 + matches.set(node, candidate) 315 + unmatched.delete(node) 316 + candidates.delete(candidate) 317 + break candidateLoop 318 + } 319 + } 311 320 } 312 321 } 313 322 } ··· 318 327 if (!isElement(node)) continue 319 328 const className = node.className 320 329 const name = node.getAttribute("name") 321 - const ariaLabel = node.getAttribute("aria-label") 322 - const ariaDescription = node.getAttribute("aria-description") 323 330 const href = node.getAttribute("href") 324 331 const src = node.getAttribute("src") 325 332 ··· 329 336 node.localName === candidate.localName && 330 337 ((className !== "" && className === candidate.className) || 331 338 (name !== "" && name === candidate.getAttribute("name")) || 332 - (ariaLabel !== "" && ariaLabel === candidate.getAttribute("aria-label")) || 333 - (ariaDescription !== "" && ariaDescription === candidate.getAttribute("aria-description")) || 334 339 (href !== "" && href === candidate.getAttribute("href")) || 335 340 (src !== "" && src === candidate.getAttribute("src"))) 336 341 ) { ··· 450 455 } 451 456 } 452 457 } 458 + } 459 + 460 + function supportsMoveBefore(_node: ParentNode): _node is NodeWithMoveBefore { 461 + return SupportsMoveBefore 453 462 } 454 463 455 464 function isDisablableElement(element: Element): element is DisablableElement {