Extracting Delivery Information from Dynamically Rendered HTML

TIL that a content script querying the DOM immediately on page load found nothing, because the POS's order details render client-side, after the content script's own 'document_idle' point had already passed.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

The extension's content script ran at document_idle, queried the DOM for order details, and consistently found nothing — not intermittently, every single time — despite the data clearly being visible on screen a moment later.

Context #

The POS is a client-rendered SPA. Its own JavaScript fetches order data and paints it into the DOM after the initial page load, meaning "the page has loaded" and "the data I need exists in the DOM" are two different points in time.

What I Tried #

Added a fixed setTimeout delay before querying the DOM, on the theory that a couple of seconds would be enough for the POS to finish rendering.

What Went Wrong #

It worked on my machine and failed for some users — because a fixed timeout is a bet on how fast the POS's own data fetch and render happens, and that varies with network conditions and server load in ways I don't control from the extension side.

The Solution #

Replaced the fixed delay with a MutationObserver watching for the specific element the extraction depends on, so the extraction runs exactly when the data actually appears rather than after a guessed delay.

function waitForOrderElement() {
  return new Promise((resolve) => {
    const existing = document.querySelector("[data-order-id]");
    if (existing) return resolve(existing);
 
    const observer = new MutationObserver(() => {
      const el = document.querySelector("[data-order-id]");
      if (el) {
        observer.disconnect();
        resolve(el);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  });
}

Why It Works #

A MutationObserver reacts to the actual DOM change instead of guessing when it'll happen, which removes the timing assumption entirely — it works the same whether the POS renders in 200ms or 3 seconds, because it's driven by the event, not a clock.

Lessons Learned #

document_idle describes when the content script itself is ready to run, not when the host page's own data has finished loading — those are unrelated timelines for a client-rendered SPA, and conflating them is what made the fixed-delay approach unreliable.

What I Would Do Differently #

I'd default to observer-based waiting for any DOM-dependent extraction against a client-rendered page from the start, rather than reaching for a timeout as the first (and, it turned out, unreliable) instinct.

MutationObserver, content script injection timing (document_start/document_end/document_idle), race conditions against client-rendered pages.