Skip to content

Commit 4b976da

Browse files
authored
perf: smart page settle via DOM stability detection (#271)
Replace fixed settleMs sleep in goto() with MutationObserver-based DOM stability detection. The page is considered settled when no DOM mutations occur for quietMs (default 500ms), with settleMs as a hard timeout cap. Changes: - Add waitForDomStableJs() shared helper to dom-helpers.ts - Update Page.goto() and CDPPage.goto() to use smart settle - No IPage interface changes (implementation detail only) Key improvements over naive approach: - Timer starts AFTER MutationObserver.observe() to avoid race condition - Falls back to sleep(maxMs) if document.body is not available - Monitors attributes in addition to childList/subtree - quietMs defaults to 500ms (conservative) for async request buffering
1 parent 3bedacc commit 4b976da

File tree

3 files changed

+48
-7
lines changed

3 files changed

+48
-7
lines changed

src/browser/cdp.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
scrollJs,
2121
autoScrollJs,
2222
networkRequestsJs,
23+
waitForDomStableJs,
2324
} from './dom-helpers.js';
2425

2526
export interface CDPTarget {
@@ -177,10 +178,11 @@ class CDPPage implements IPage {
177178
.catch(() => {}); // Don't fail if event times out
178179
await this.bridge.send('Page.navigate', { url });
179180
await loadPromise;
180-
// Post-load settle: SPA frameworks need extra time to render after load event
181+
// Smart settle: use DOM stability detection instead of fixed sleep.
182+
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
181183
if (options?.waitUntil !== 'none') {
182-
const settleMs = options?.settleMs ?? 1000;
183-
await new Promise(resolve => setTimeout(resolve, settleMs));
184+
const maxMs = options?.settleMs ?? 1000;
185+
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
184186
}
185187
}
186188

src/browser/dom-helpers.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,37 @@ export function networkRequestsJs(includeStatic: boolean): string {
145145
})()
146146
`;
147147
}
148+
149+
/**
150+
* Generate JS to wait until the DOM stabilizes (no mutations for `quietMs`),
151+
* with a hard cap at `maxMs`. Uses MutationObserver in the browser.
152+
*
153+
* Returns as soon as the page stops changing, avoiding unnecessary fixed waits.
154+
* If document.body is not available, falls back to a fixed sleep of maxMs.
155+
*/
156+
export function waitForDomStableJs(maxMs: number, quietMs: number): string {
157+
return `
158+
new Promise(resolve => {
159+
if (!document.body) {
160+
setTimeout(() => resolve('nobody'), ${maxMs});
161+
return;
162+
}
163+
let timer = null;
164+
let cap = null;
165+
const done = (reason) => {
166+
clearTimeout(timer);
167+
clearTimeout(cap);
168+
obs.disconnect();
169+
resolve(reason);
170+
};
171+
const resetQuiet = () => {
172+
clearTimeout(timer);
173+
timer = setTimeout(() => done('quiet'), ${quietMs});
174+
};
175+
const obs = new MutationObserver(resetQuiet);
176+
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
177+
resetQuiet();
178+
cap = setTimeout(() => done('capped'), ${maxMs});
179+
})
180+
`;
181+
}

src/browser/page.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
scrollJs,
2424
autoScrollJs,
2525
networkRequestsJs,
26+
waitForDomStableJs,
2627
} from './dom-helpers.js';
2728

2829
/**
@@ -53,11 +54,15 @@ export class Page implements IPage {
5354
if (result?.tabId) {
5455
this._tabId = result.tabId;
5556
}
56-
// Post-load settle: the extension already waits for tab.status === 'complete',
57-
// but SPA frameworks (React/Vue) need extra time to render after DOM load.
57+
// Smart settle: use DOM stability detection instead of fixed sleep.
58+
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
5859
if (options?.waitUntil !== 'none') {
59-
const settleMs = options?.settleMs ?? 1000;
60-
await new Promise(resolve => setTimeout(resolve, settleMs));
60+
const maxMs = options?.settleMs ?? 1000;
61+
await sendCommand('exec', {
62+
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
63+
...this._workspaceOpt(),
64+
...this._tabOpt(),
65+
});
6166
}
6267
}
6368

0 commit comments

Comments
 (0)