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?
Advertisement
Answer
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.