Precise DOM morphing
morphing typescript dom

Performance improvements

+28 -9
+28 -9
src/morphlex.ts
··· 129 129 } 130 130 131 131 private morphOneToOne(from: ChildNode, to: ChildNode): void { 132 + // Fast path: if nodes are exactly the same object, skip morphing 133 + if (from.isSameNode?.(to)) return 134 + 132 135 if (!(this.options.beforeNodeMorphed?.(from, to) ?? true)) return 133 136 134 137 const pair: PairOfNodes<ChildNode> = [from, to] ··· 167 170 } 168 171 169 172 private morphAttributes([from, to]: PairOfMatchingElements<Element>): void { 170 - // Remove any excess attributes from the element that aren’t present in the reference. 171 - for (const { name, value } of from.attributes) { 172 - if (!to.hasAttribute(name) && (this.options.beforeAttributeUpdated?.(from, name, null) ?? true)) { 173 - from.removeAttribute(name) 174 - this.options.afterAttributeUpdated?.(from, name, value) 175 - } 176 - } 173 + const toAttrs = to.attributes 174 + const fromAttrs = from.attributes 177 175 178 - // Copy attributes from the reference to the element, if they don’t already match. 179 - for (const { name, value } of to.attributes) { 176 + // First pass: update/add attributes from reference (iterate forwards) 177 + for (let i = 0; i < toAttrs.length; i++) { 178 + const attr = toAttrs[i]! 179 + const name = attr.name 180 + const value = attr.value 180 181 const oldValue = from.getAttribute(name) 182 + 181 183 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 182 184 from.setAttribute(name, value) 183 185 this.options.afterAttributeUpdated?.(from, name, oldValue) 186 + } 187 + } 188 + 189 + // Second pass: remove excess attributes (iterate backwards for efficiency) 190 + for (let i = fromAttrs.length - 1; i >= 0; i--) { 191 + const attr = fromAttrs[i]! 192 + const name = attr.name 193 + const value = attr.value 194 + 195 + if (!to.hasAttribute(name) && (this.options.beforeAttributeUpdated?.(from, name, null) ?? true)) { 196 + from.removeAttribute(name) 197 + this.options.afterAttributeUpdated?.(from, name, value) 184 198 } 185 199 } 186 200 } ··· 264 278 const toChildNode = toChildNodes[i]! 265 279 266 280 if (fromChildNode) { 281 + // Fast path: if nodes are exactly the same, skip morphing 282 + if (fromChildNode.isSameNode?.(toChildNode)) { 283 + continue 284 + } 285 + 267 286 if (isElement(toChildNode)) { 268 287 this.searchSiblingsToMorphChildElement(fromChildNode, toChildNode, from) 269 288 } else {