From 868a1f83d183d661063a4061e6674c9f75f91829 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 5 May 2026 16:11:54 +0200 Subject: [PATCH] Skip beforeAttributeUpdated when an attribute value is unchanged morphAttributes called the beforeAttributeUpdated callback for every attribute on every element being morphed, before checking whether the incoming value differed from the existing one. The callback's purpose is to let consumers veto a write - there is nothing to veto when the value is already equal, and on dense pages the callback is a hot path. --- src/idiomorph.js | 7 ++++--- test/hooks.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 2994d51..dd6975c 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -676,12 +676,13 @@ var Idiomorph = (function () { const oldAttributes = oldElt.attributes; const newAttributes = newElt.attributes; for (const newAttribute of newAttributes) { - if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) { + if (oldElt.getAttribute(newAttribute.name) === newAttribute.value) { continue; } - if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) { - oldElt.setAttribute(newAttribute.name, newAttribute.value); + if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) { + continue; } + oldElt.setAttribute(newAttribute.name, newAttribute.value); } // iterate backwards to avoid skipping over items when a delete occurs for (let i = oldAttributes.length - 1; 0 <= i; i--) { diff --git a/test/hooks.js b/test/hooks.js index a41b078..b80734d 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -235,6 +235,34 @@ describe("lifecycle hooks", function () { initial.outerHTML.should.equal(``); }); + it("does not call beforeAttributeUpdated when the attribute value is unchanged", function () { + let calls = []; + let initial = make(``); + Idiomorph.morph(initial, ``, { + callbacks: { + beforeAttributeUpdated: (attributeName, node, mutationType) => { + calls.push([attributeName, mutationType]); + }, + }, + }); + initial.outerHTML.should.equal(``); + calls.should.eql([]); + }); + + it("calls beforeAttributeUpdated only for attributes that actually change", function () { + let calls = []; + let initial = make(``); + Idiomorph.morph(initial, ``, { + callbacks: { + beforeAttributeUpdated: (attributeName, node, mutationType) => { + calls.push([attributeName, mutationType]); + }, + }, + }); + initial.outerHTML.should.equal(``); + calls.should.eql([["href", "update"]]); + }); + it("hooks work as expected", function () { let beginSrc = `