How to guarantee that deferred JS scripts have executed?

404 Views Asked by At

On my website I use javascript modules, which according to MDN are deferred by default. I have some additional javascript which can only safely execute once these modules have loaded and executed. MDN indicates here that deferred script execution is guaranteed to have occurred by the time the DOMContentLoaded event has fired. Further, it is suggested here that you can also account for the case where this event has already fired like so:

function doSomething() {
  console.info('DOM loaded');
}

if (document.readyState === 'loading') {  // Loading hasn't finished yet
  document.addEventListener('DOMContentLoaded', doSomething);
} else {  // `DOMContentLoaded` has already fired
  doSomething();
}

The block above seems to be what I'm looking for. But documentation elsewhere on MDN leaves me unsure as to whether this is really correct. For instance, why does the readystate check above look for loading rather than loading OR interactive? The documentation on readystate says that loading is followed by interactive, and that in the interactive state " sub-resources such as scripts, images, stylesheets and frames are still loading."

So it seems to me that there is an inconsistency here. Either the suggested readystate check is not sufficient to guarantee that DOMContentLoaded has fired, or DOMContentLoaded is not sufficient to guarantee that deferred scripts have completed.

2

There are 2 best solutions below

0
CertainPerformance On

When readyState is at least interactive, that means that the document has been fully parsed, and that all the elements in the source HTML now exist in the DOM.

Usually, people implementing this solution are doing so because they're trying to attach listeners to elements, and need to wait for all elements to exist in the DOM - and so all they need to do is check document.readyState === 'loading'.

If you also want to wait for <script type="module"> scripts to run, that's a different problem, with a different solution.

The best way by far would be to have a single entry point for your app's in a module, so that you don't have to worry about loading order at all - it'll just work.

If you really have to determine when all module scripts have run from a non-module script (which I wouldn't recommend), you'd have to iterate over them and listen to their load events.

// run this at the end of the body -
// once all script tags exist, but before they've run
Promise.all(
  [...document.querySelectorAll('script[type="module"]')]
    .map(script => new Promise(
      resolve => script.addEventListener('load', resolve)
    ))
)
  .then(() => {
    // all module scripts are loaded
   })

But that's quite convoluted and probably isn't a good approach.

If you listen to the load event for the window instead, you'll also be waiting for all other resources to load as well (images and stylesheets and such) which isn't desirable.

0
Jonas Wilms On
 import "other-module";
 // module is guaranteed to be loaded here

By importing the module one can guarantee that it is loaded before the module the import is in runs.