From d75241467cd2ebe71cceed5219f29db0bf1c2287 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Tue, 14 Jul 2026 02:43:47 +1200 Subject: [PATCH 1/3] improve hx-live perf by skipping no change updates --- src/ext/hx-live.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ext/hx-live.js b/src/ext/hx-live.js index bf3671b7c..04d3a895e 100644 --- a/src/ext/hx-live.js +++ b/src/ext/hx-live.js @@ -18,10 +18,16 @@ const OBSERVE_OPTIONS = { childList: true, subtree: true, attributes: true, characterData: true }; + let inputDebounceId = null; + const INPUT_DEBOUNCE_MS = htmx.config.live?.inputDebounceMs ?? 100; + function ensureActive() { if (observer) return; recomputeBound = () => schedule(); - document.addEventListener('input', recomputeBound, true); + document.addEventListener('input', () => { + clearTimeout(inputDebounceId); + inputDebounceId = setTimeout(schedule, INPUT_DEBOUNCE_MS); + }, true); document.addEventListener('change', recomputeBound, true); observer = new MutationObserver(recomputeBound); observer.observe(document.documentElement, OBSERVE_OPTIONS); @@ -29,6 +35,8 @@ function deactivate() { if (!observer) return; + clearTimeout(inputDebounceId); + inputDebounceId = null; document.removeEventListener('input', recomputeBound, true); document.removeEventListener('change', recomputeBound, true); observer.disconnect(); @@ -112,6 +120,7 @@ let value = rest[0]; for (let e of elts) { + if (applyAttr([e], name) === value) continue; if (isClass) { e.classList.toggle(name.slice(1), !!value); if (e.classList.length === 0) e.removeAttribute('class'); @@ -595,8 +604,16 @@ } function writeAttrBinding(elt, attrName, value) { - if (attrName === 'text') { elt.textContent = value == null ? '' : String(value); return; } - if (attrName === 'html') { elt.innerHTML = value == null ? '' : String(value); return; } + if (attrName === 'text') { + let s = value == null ? '' : String(value); + if (elt.textContent !== s) elt.textContent = s; + return; + } + if (attrName === 'html') { + let s = value == null ? '' : String(value); + if (elt.innerHTML !== s) elt.innerHTML = s; + return; + } if (attrName === 'style') { applyStyleBinding(elt, value); return; } // Everything else (class, .class, aria-*, boolean, property-sync, regular) → applyAttr. applyAttr([elt], attrName, value); From 648db6e28d8b9a3961de32efdc1aaeb0d6385405 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Tue, 14 Jul 2026 03:27:31 +1200 Subject: [PATCH 2/3] fix aria and checkbox handling --- src/ext/hx-live.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ext/hx-live.js b/src/ext/hx-live.js index 04d3a895e..2d748e684 100644 --- a/src/ext/hx-live.js +++ b/src/ext/hx-live.js @@ -120,7 +120,6 @@ let value = rest[0]; for (let e of elts) { - if (applyAttr([e], name) === value) continue; if (isClass) { e.classList.toggle(name.slice(1), !!value); if (e.classList.length === 0) e.removeAttribute('class'); @@ -615,7 +614,9 @@ return; } if (attrName === 'style') { applyStyleBinding(elt, value); return; } - // Everything else (class, .class, aria-*, boolean, property-sync, regular) → applyAttr. + // Always write aria-* and property-backed attrs (getter type differs from setter). + // For everything else skip if unchanged. + if (!attrName.startsWith('aria-') && !PROPERTY_ATTRS.has(attrName) && applyAttr([elt], attrName) === value) return; applyAttr([elt], attrName, value); } From 79269d7f1edd2980386c9c824768794fd3574a2a Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Tue, 14 Jul 2026 10:37:14 +1200 Subject: [PATCH 3/3] fix input event listener disconnect --- src/ext/hx-live.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ext/hx-live.js b/src/ext/hx-live.js index 2d748e684..045ccc1d8 100644 --- a/src/ext/hx-live.js +++ b/src/ext/hx-live.js @@ -11,6 +11,7 @@ let dbSym = Symbol(); let observer = null; let recomputeBound = null; + let inputBound = null; let swaps = 0; let i = 0; let start = 0; @@ -24,10 +25,8 @@ function ensureActive() { if (observer) return; recomputeBound = () => schedule(); - document.addEventListener('input', () => { - clearTimeout(inputDebounceId); - inputDebounceId = setTimeout(schedule, INPUT_DEBOUNCE_MS); - }, true); + inputBound = () => { clearTimeout(inputDebounceId); inputDebounceId = setTimeout(schedule, INPUT_DEBOUNCE_MS); }; + document.addEventListener('input', inputBound, true); document.addEventListener('change', recomputeBound, true); observer = new MutationObserver(recomputeBound); observer.observe(document.documentElement, OBSERVE_OPTIONS); @@ -37,7 +36,8 @@ if (!observer) return; clearTimeout(inputDebounceId); inputDebounceId = null; - document.removeEventListener('input', recomputeBound, true); + document.removeEventListener('input', inputBound, true); + inputBound = null; document.removeEventListener('change', recomputeBound, true); observer.disconnect(); observer = null;