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.
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>
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
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.
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.