I am writing a user script which needs to access specific DOM nodes on the page. To reduce latency and page re-layouting, I want to set it to // @run-at document-start
instead of the usual // @run-at document-end
, and run my code as soon as the DOM node I need has been downloaded from the server.
If I just wait for a DOMContentLoaded
event, it will wait for the whole page to be downloaded; this is effectively the same as // @run-at document-end
. I want to run my code at an earlier time, right when the browser’s parser encounters the closing tag of the element I want.
Is there a way to accomplish this more elegant than polling querySelectorAll
from a MutationObserver
callback?
CodePudding user response:
This is what I currently use:
const element = (selector, pred = () => true, below = document) =>
new Promise((accept, reject) => {
const mo = new MutationObserver((mutations, self) => {
for (const node of below.querySelectorAll(selector)) {
if (!pred(node))
continue;
accept(node);
self.disconnect();
return;
}
}).observe(below, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
});
document.addEventListener('DOMContentLoaded', ev => {
reject();
if (mo) mo.disconnect();
});
});
The above returns a promise that resolves when a node fulfilling my criteria first appears in the DOM tree. To await for the moment when it is fully downloaded, I additionally pass it to:
const nodeComplete = (node) => new Promise((accept, reject) => {
if (document.readyState === "complete" || document.readyState === "loaded") {
accept();
return;
}
document.addEventListener('DOMContentLoaded', ev => {
accept();
if (mo) mo.disconnect();
});
const mo = new MutationObserver((mutations, self) => {
let anc = node;
while (anc) {
if (anc.nextSibling) {
accept();
self.disconnect();
}
anc = anc.parentNode;
}
}).observe(document, {
childList: true,
subtree: true,
});
});
Not terribly great, but serviceable; I imagine some optimizations can be applied here.