Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions src/ext/hx-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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 {
Expand Down
51 changes: 33 additions & 18 deletions test/tests/ext/hx-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<output hx-live="window.__runawayCountLive++"></output>';
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('<output hx-live=""></output>');

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;
}
});
Expand Down
10 changes: 8 additions & 2 deletions www/src/content/extensions/06-hx-live.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading