Skip to content
Advertisement

How can I detect the moment a specific DOM node has been downloaded by the browser, while the page is still loading?

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.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement