Skip to content

Instantly share code, notes, and snippets.

@developit
Last active November 30, 2023 07:30
Show Gist options
  • Save developit/92410b241def20a40ac15daeaeb932db to your computer and use it in GitHub Desktop.
Save developit/92410b241def20a40ac15daeaeb932db to your computer and use it in GitHub Desktop.

Modern Script Loading

Loading modern code for modern browsers while still supporting older browsers should be possible via module/nomodule:

<script type="module" src="/modern.js"></script>
<script nomodule src="/legacy.js"></script>

... however this results in over-fetching of scripts in Edge and Safari.

Option 1: Load Dynamically

We can circumvent these issues by implementing a tiny script loader, similar to what we do with LoadCSS:

<!-- preload the modern script in browsers that support modules: -->
<link rel="modulepreload" href="/modern.js">

<!-- use a module script to detect modern browsers: -->
<script type=module>self.modern=1</script>

<!-- now use that flag to load modern VS legacy code: -->
<script>
  document.head.appendChild((function(s){
    if (self.modern){ s.src='/modern.js'; s.type='module'; }
    else s.src = '/legacy.js'
  })(document.createElement('script')))
</script>

Here's what that might look like in prod:

<script type=module>self.modern=1</script>
<script>
  function loadJS(e,d,c){c=document.createElement("script"),self.modern?(c.src=e,c.type="module"):c.src=d,document.head.appendChild(c)}
  loadJS('/bundle.js', '/bundle.legacy.js')
</script>

Option 2: UA Sniffing

I don't have a code sample for this, since User Agent detection is nontrivial. Essentially, this technique uses the same <script src=bundle.js> for all browsers, but when bundle.js is requested, the server inspects the browser's User Agent string and actually serves up modern or legacy JavaScript depending on whether it's recognized as a modern browser or not.

While this approach is versatile, it comes with some severe implications:

  • since server smarts are required, this doesn't work for static deployment (static site generators, Netlify, etc)
  • caching for those JavaScript URLs now varies based on User Agent, which is highly volitile
  • UA detection is difficult and can be prone to false classification
  • the User Agent string is easily spoofable and new UA's arrive daily

Option 3: Penalize older browsers

The ill-effects of module/nomodule are seen in old versions of Chrome, Firefox and Safari - these browser versions have extremely limited usage, since they employ automatic updates. This doesn't hold true for Edge 16-18, but new versions of Edge will use a Chromium-based renderer that doesn't suffer from this issue.

It might be reasonable for some applications to accept this as a trade-off: you get to deliver modern code to 90% of browsers, at the expense of some extra bandwidth on older browsers. Notably, none of the User Agents suffering from this over-fetching issue have significant mobile marketshare - so those bytes are less likely to be coming from an expensive mobile plan or through a device with a slow processor.

Option 4: Use for conditional bundles

One clever approach here is to use nomodule to conditionally load bundles of code that isn't required in modern browsers, such as polyfills. With this approach, the worst-case is that the polyfills are loaded or possibly even executed (in Safari 10.3, for example), but the effect is limited to "over-polyfilling". Argubaly, compared to always polyfilling, that's a net improvement.

<!-- newer browsers won't load this bundle: -->
<script nomodule src="polyfills.js"></script>
<!-- all browsers load this one: -->
<script src="/bundle.js"></script>

Angular CLI can be configured to use this approach for polyfills, as demonstrated by Minko Gechev.

function loadJS(modernSrc, fallbackSrc, s) {
s = document.createElement('script');
if (self.modern) {
s.src = modernSrc;
s.type = 'module';
}
else {
s.src = fallbackSrc;
}
document.head.appendChild(s);
}
function loadJS(e,d,c){c=document.createElement("script"),self.modern?(c.src=e,c.type="module"):c.src=d,document.head.appendChild(c)}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment