Modern Script Loading

Last updated 3 years ago by Jason Miller


Serving the right code to the right browsers can be tricky. Here are some options.

Serving modern code to modern browsers can be great for performance. Your JavaScript bundles can contain more compact or optimized modern syntax, while still supporting older browsers.

The tooling ecosystem has consolidated on using the module/nomodule pattern for declaratively loading modern VS legacy code, which provides browsers with both sources and lets them decide which to use:


``` Unfortunately, it's not quite that straightforward. The HTML-based approach shown above triggers [over-fetching of scripts in Edge and Safari]( ### What can we do? What can we do? We want to deliver two compile targets depending on the browser, but a couple older browsers don't quite support the nice clean syntax for doing so. First, there's the [Safari Fix]( Safari 10.1 supports JS Modules not the `nomodule` attribute on scripts, which causes it to execute both the modern and legacy code _(yikes!)_. Thankfully, Sam found a way to use a non-standard `beforeload` event supported in Safari 10 & 11 to polyfill `nomodule`. #### Option 1: Load Dynamically We can circumvent these issues by implementing a tiny script loader, similar to how [LoadCSS]( works. Instead of relying on browsers to implement both ES Modules and the `nomodule` attribute, we can attempt to execute a Module script as a "litmus test", then use the result of that test to choose whether to load modern or legacy code. ```language-html


However, this solution requires waiting until our first "litmus test" module script has run before it can inject the correct script. This is because <script type=module> is always asynchronous. There is a better way!

A standalone variant of this can be implemented by checking if the browser supports nomodule. This would mean browsers like Safari 10.1 are treated as legacy even though they support Modules, but that might be a good thing. Here's the code for that:

language-js var s = document.createElement('script') if ('noModule' in s) { // notice the casing s.type = 'module' s.src = '/modern.js' } else s.src = '/legacy.js' } document.head.appendChild(s)

This can be quickly rolled into a function that loads modern or legacy code, and also ensures both are loaded asynchronously:



What's the trade-off? preloading.

The trouble with this solution is that, because it's completely dynamic, the browser won't be able to discover our JavaScript resources until it runs the bootstrapping code we wrote to inject modern vs legacy scripts. Normally, browsers scan HTML as it is being streamed to look for resources they can preload. There's a solution, though it's not perfect: we can use <link rel=modulepreload> to preload the modern version of a bundle in modern browsers. Unfortunately, only Chrome supports modulepreload so far.



Whether this technique works for you can come down to the size of the HTML document you're embedding those scripts into. If your HTML payload is as small as a splash screen or just enough to bootstrap a client-side application, giving up the preload scanner is less likely to impact performance. If you are server-rendering a lot of meaningful HTML for the browser to stream, the preload scanner is your friend and this might not be the best approach for you.

Read full Article