1

Disclaimer: I am aware of why should I avoid using document.write and document.write clears page.

Let's say I need to inject a synchronous (blocking) script into the DOM when it's being loaded (document.readyState = 'loading'), but right before another specific existing sync script.

If I have access to HTML code, I can do this by writing something like:

<html>
  <head>
    <title>It's okay</title>
    <script>
      document.write(
        '<script src="https://www.googletagmanager.com/gtm.js?id=GTM-WZNT8QT" onload="console.log(1)"><'+'/script>'
      );
    </script>
    <script src="https://code.jquery.com/jquery-3.6.3.min.js" onload="console.log(2)"></script>
  </head>
  <body>
    <script>console.log('DOM is parsed')</script>
  </body>
</html>

It correctly loads both JS files, and outputs 1, 2, DOM is parsed to the console.

Now, if I try to do it using MutationObserver (that is, detecting when a new script node is inserted, but before the script is actually invoked),

<html>
  <head>
    <title>Why not loading?</title>
    <script>
      // Say, this JS is invoked by the extension before even parsing HTML
      new MutationObserver((mutationList) => {
        for (const mutation of mutationList) {
          for (const node of mutation.addedNodes) {
            if (node.tagName !== 'SCRIPT' || !node.src.includes('jquery')) {
              continue;
            }
            document.write(
              '<script src="https://www.googletagmanager.com/gtm.js?id=GTM-WZNT8QT" onload="console.log(1)"><'+'/script>'
            );
          }
        }
      }).observe(document, { childList: true, subtree: true });
    </script>
    <script src="https://code.jquery.com/jquery-3.6.3.min.js" onload="console.log(2)"></script>
  </head>
  <body>
    <script>console.log('Now DOM is parsed!')</script>
  </body>
</html>

...not only it makes the browser "always loading" something, but also never prints 2 and Now DOM is parsed! to the console. Upon checking the actual HTML code, there's only that "googletagmanager" script in the head. JavaScript thread is not blocked. And actually document.readyState always stays 'loading' from that moment, effectively stuck and broken.

Always loading

Questions:

  1. Why is this happening? How can you explain it? My expectation was that both scripts would be invoked.
  2. Is there any way to load a synchronous piece of code before the specific script (already present in HTML code) is executed? (with not having access to editing HTML but having an ability to inject JS before HTML is even parsed, say, browser extension etc).

With MutationObserver actually, I can at least prevent the jquery code from running by saying node.setAttribute('src', 'text/prevented') in the mutation handler, so it doesn't matter whether googletagmanager script is inserted before or after it. I am trying to understand why it doesn't work.

ZitRo
  • 1,163
  • 15
  • 24
  • Try using the deprecated DOMNodeInserted. It's synchronous so it might play well with document.write. – wOxxOm Mar 14 '23 at 01:49
  • @Yogi no. As explained, when document.write happens the document is still in the "loading" state, i.e. stream should be open. So this is why I ask why it works this way. – ZitRo Mar 15 '23 at 00:48
  • Thanks @Yogi and @wOxxOm. @wOxxOm, DOMNodeInserted unfortunately doesn't seem to pick up already-present elements in DOM, or at least my attempts to `document.addEventListener("DOMSubtreeModified", ...)` failed. Deprecated API also feels a bit dangerous to use. @Yogi definitely agreed for discouraged use. However, with my use case I am trying to cover "old good websites", like those which are still using JQuery and have sync scripts in the DOM, which is also bad. Thanks for referencing the rules – might be, my use case is so unintended that it's simply not handled properly by browsers (a bug). – ZitRo Mar 15 '23 at 12:27

0 Answers0