A Universal Bundle Loader
This is a little follow up for my previous post, targeting those developers wondering about the usage of
<script type=module> to simply distinguish between modern browsers and not.
The ES2017 Bar
Even if ECMAScript Modules have been defined already in ES2015, their implemntation on the Web landed between 2017 and 2018.
If you check targets browsers able to understand the new script type, you realize around 75% of the browsers would work there, leaving out though UC Browser and Samsung browser on mobile, both a very relevant piece of mobile market.
Accordingly, I’d say it’s still a bit early to cut out all non ESM compatible browsers, and after all, ES2016+ didn’t bring in anything that special that couldn’t be transpiled down to ES2015 with relatvely ease.
The ES2015 Bar
This is the sweet spot where most common features will just work, and few others can be transpiled with ease, including
await, usually the reason developers would love to stick with ES2017+.
As previously mentioned though, the most important thing to preserve in ES2015 are classes, that regardless everyone on the web sold them as “syntactical sugar”, ES6 classes are a curse for both TypeScript and Babel to transform, and full of hidden evil gotchas.
But covering around 88% of the browsers, including UC Browser and Samsung Internet, we can stop avoiding classes, extending native constructors, or patching the whole DOM, because we won’t need anymore to fallback to the old
function based classes that are so similar, and yet so different, from the modern native one.
While the major point of my previous post was to demonstrate there are techniques to manually target with ease modern and legacy browsers, using ES2015 as the line in beteween, few developers came back with the usual story that
document.write is ugly and yada yada, missing the whole point that my technique would not ever touch modern browsers, but will simply always work reliably on legacy.
However, I have to admit posting about
I’ve called it: Universal Bundle Loader, UBL for short.
<!-- targeting native async/await as ES2017 target -->
<script type="module" src="es2017.js"></script>
<!-- targeting ES2015 with ES5 fallback -->
// Safari 10.1 is compatible with ESM
// but it ignores the nomodule attribute
// grab last script node
// append before it a new script (IE < 9 compatible)
// set it up deferred and with a good'ol mime
// load the ES2015 bundle, fallback to the ES5 one
Above technique is 100% compatible with pretty much every browser that ever parsed JS since ES3 era, and even if it shows a triple bundle solution, something that actually shouldn’t really be too hard to automate, if we can already automate 2 different bundles, it can be used to serve only two versions of our bundles, one targeting ES2015 and one targeting ES5 (or even lower than ES5).
You can test it live in here. (Update: current live page is using an alternative technique described later on in this post)
To reduce the technique in two bundles insteda of 3, either keep the same output but use
es2015.js also as
type=module source, or drop the script module and remove the
nomodule attribute, together with the User Agent sniffing bit, to decide at runtime which bundle should go in.
About that User Agent sniff …
I can hear already someone screaming something along the line:
“Oh come on, you replaced
document.write with UA sniffing? Bad dev!”
First of all, I haven’t really removed the
document.write technique described in the previous post, because to bring in on demand common polyfills, the technique is still valid and usable.
Secondly, the day you will find a real-world issue with that UA sniffing is the day I’ll consider improving it, but if I were you, I would not drop that sniff, unless you decided that Safari 10.1 isn’t interesting for your business.
And yet, I don’t think any of you thinks bringing in this polyfill for that browser only would be a wiser choice than a pragmatic UA sniff that target a single browser, isn’t it?
Without User Agent sniff
Using an intermediate external file, we can avoid the UA sniffing by detecting if the ESM code previously run.
<script nomodule defer src="ubl.js"></script>
The first script will flag
true and import the ES2017 bundle, while the second file will contain similar code shown before.
The key, beside using
nomodule attribute, is to add
defer to the mix so that it eventually runs, in case
nomodule is not respected, instantly after the module has been parsed.
That would avoid any browser downloading, even by accident, anything more than just that little closure which will not bring in any file.
Other Techniques or Alternative Opinions
Still coming from my previous post, I’ve described more techniques to feature detect and bring in on demand any polyfill you want through this repository.
You can also find, in the same repo, a link to another post which aim is very similar to mine in my last two posts.
I am talking about Philip Walton post titled Deploying ES2015+ Code in Production Today, where he focues mostly on delivering ES2017+ instead of the most widely available ES2015, which I’m pushing for in here too.
It’s safe, and Web compatible, to target 2015 capable browsers, leaving out mostly only IE, but not Edge, something fading away in 2018.
It will probably be just enough in 2020 to target ES2017+ browsers, splitting between regular
<script type=module> and
<script nomodule>, but we’re not quite there yet.
In few words, even if you use
babel-preset-env, I think for production sake it’s better, and safer, to target last 3 years browsers, instead of last 3 versions, since versions lost their meaning in evergreen browsers, and who’s not evergreen won’t easily catch up anyway.
With the Universal Bundle Loader approach though, you can already target legacy, oldish, and modern browesers, covering the entirity of the Web.