diff --git a/src/ext/hx-live.js b/src/ext/hx-live.js
index fc58c4e9c..8934db67c 100644
--- a/src/ext/hx-live.js
+++ b/src/ext/hx-live.js
@@ -13,8 +13,6 @@
let recomputeBound = null;
let inputBound = null;
let swaps = 0;
- let i = 0;
- let start = 0;
let warned = false;
const OBSERVE_OPTIONS = { childList: true, subtree: true, attributes: true, characterData: true };
@@ -42,26 +40,23 @@
observer.disconnect();
observer = null;
recomputeBound = null;
+ warned = false;
}
function schedule() {
if (pending) return;
if (swaps > 0) return;
- let now = Date.now();
- if (now - start > 1000) {
- start = now;
- i = 0;
- warned = false;
- }
- if (++i > 50 && !warned) {
- console.warn('htmx: hx-live recompute exceeded 50/sec.');
- warned = true;
- }
pending = true;
queueMicrotask(() => {
// Detach observer while writing so our own writes don't queue records.
observer?.disconnect();
+ let startedAt = performance.now();
fns.forEach(f => f());
+ let elapsed = performance.now() - startedAt;
+ if (!warned && elapsed > 16) {
+ console.warn(`htmx: hx-live expressions took ${elapsed.toFixed(1)}ms.`);
+ warned = true;
+ }
if (fns.size === 0) {
deactivate();
} else {
diff --git a/test/tests/ext/hx-live.js b/test/tests/ext/hx-live.js
index 6c00b2062..e12e89f27 100644
--- a/test/tests/ext/hx-live.js
+++ b/test/tests/ext/hx-live.js
@@ -342,27 +342,42 @@ describe('hx-live extension', function () {
delete window.__swapCountLive;
});
- it.skip('iteration cap warns on runaway', async function() {
- let warned = false;
- let originalWarn = console.warn;
- console.warn = (...args) => {
- if (typeof args[0] === 'string' && args[0].includes('hx-live recompute exceeded')) warned = true;
- originalWarn.apply(console, args);
- };
- try {
- window.__runawayCountLive = 0;
- playground().innerHTML = '';
- htmx.process(playground());
+ it('warns once when live expressions take more than 16ms', async function() {
+ htmx.live.refresh();
+ await Promise.resolve();
- for (let i = 0; i < 100; i++) {
- document.body.setAttribute('data-runaway-test-live', String(i));
- await htmx.timeout(5);
- }
+ let originalNow = Object.getOwnPropertyDescriptor(performance, 'now');
+ let originalWarn = console.warn;
+ let now = 0;
+ let elapsed = 0;
+ let calls = 0;
+ let warnings = [];
+ Object.defineProperty(performance, 'now', {
+ configurable: true,
+ value: () => calls++ % 2 === 0 ? now : now += elapsed
+ });
+ console.warn = message => warnings.push(message);
- warned.should.equal(true);
- document.body.removeAttribute('data-runaway-test-live');
- delete window.__runawayCountLive;
+ try {
+ createProcessedHTML('');
+
+ elapsed = 16;
+ htmx.live.refresh();
+ await Promise.resolve();
+ warnings.should.deep.equal([]);
+
+ elapsed = 16.1;
+ htmx.live.refresh();
+ await Promise.resolve();
+ warnings.should.deep.equal(['htmx: hx-live expressions took 16.1ms.']);
+
+ elapsed = 50;
+ htmx.live.refresh();
+ await Promise.resolve();
+ warnings.length.should.equal(1);
} finally {
+ if (originalNow) Object.defineProperty(performance, 'now', originalNow);
+ else delete performance.now;
console.warn = originalWarn;
}
});
diff --git a/www/src/content/extensions/06-hx-live.md b/www/src/content/extensions/06-hx-live.md
index 1680fedde..6bed895b0 100644
--- a/www/src/content/extensions/06-hx-live.md
+++ b/www/src/content/extensions/06-hx-live.md
@@ -545,9 +545,15 @@ All expressions run in a single microtask, so multiple synchronous mutations coa
When an expression writes to the DOM, the observer drains its own pending records inside the same microtask. Writes made by `hx-live` cannot trigger a feedback loop.
-### Runaway cap
+### Slow expressions
-If recomputes exceed 50/sec, the extension logs a warning. Bindings continue running. Tune your expression or add `debounce`.
+After a change, hx-live runs every live expression once. If this takes more than `16ms`, hx-live logs one warning:
+
+```text
+htmx: hx-live expressions took 18.4ms.
+```
+
+The warning does not stop the expressions.
### Coordinating with htmx swaps