Have you heard about ES6 modules in the browser? They’re like ES6 modules, but in the browser.
For those not in the know, this is what they look like:
They’ve been around for years, and maybe you’re wondering if you should dip your toes in. Well, I’ve been using them for the last month or so on a side-project, and have come the conclusion that …
First things first, the browser support: as I write this at the start of 2020, it’s a pretty respectable 90%.
I’m quite happy with 90% coverage (I don’t like 10% of people), but maybe you want to be more responsible. Even then, if your project doesn’t aim to support IE or UC or mini, that support stat gets pretty close to 100% of your target audience.
But browser support is only the beginning. In this post, I’ll aim to answer the three questions that I had at the beginning of my journey:
- Is there any benefit to using modules in the browser?
- Are there any downsides?
- I’m certain I had a third question but I don’t recall now.
Let’s get into it …
What are the benefits?
There’s something refreshing about writing code that will actually be run in the browser, as authored. It might take a wee bit more effort, but it’s really quite satisfying. It’s like driving a manual car or maintaining your own peep of chickens (call me crazy, but an egg tastes better if you eat it while looking its mother in the eye).
There’s nothing else.
What are the downsides?
First: modules vs bundlers
When considering whether or not to use ES6 modules in the browser, it’s really a decision between using modules or a bundler like Webpack or Parcel or Rollup.
You could (maybe) do both, but in reality, if you’re running your code through a bundler, there’s no reason to use
<script type="module"> to load those bundled files.
So, assuming that you’re thinking about using ES6 modules instead of a bundler, here’s the things that you’ll give up:
- Minified output
- npm modules
Let’s look at three of the five in more detail:
My project, with modules, is 34 KB. Since I have no build pipeline, it’s coming across the network with comically long variable names and comments galore, and in lots of little files, which is not ideal for compression.
If I bundle these files up with Parcel, the payload comes down to 18 KB. My Casual Casio™ calculator says that’s “about half”. This is party due to Parcel minifying my files, but also more efficient gzipping.
The bit about gzipping highlights another problem: the way that you organise your files (for a sensible developer experience) directly translates to the way your files are transferred over the network. And you don’t necessarily want this. It might make sense for a project to have 150 modules (files), but ship those to the browser wrapped up as 12 individual bundles.
To reiterate, I’m not saying that using modules means you can’t bundle and minify your files (any preclusion delusion is an illusion), just that there’s no point in doing both.
Funny story: my app was using modules right up until I wrote that last section. I installed Parcel so I could tell you the correct build sizes, and now I see no reason to go back to plain modules. Funny!
A life without transpiling
Over the years, I’ve gotten so used to using the latest syntax — and relying on the amazing Babel to turn that into something all browsers can use — that I rarely even think about browser support, (except for DOM and CSS, obviously).
When I first set out using
But the reality has been not quite so rosy. Quick quiz: does Edge support
flatMap()? Does Safari support destructuring assignment (objects and arrays)? Does Chrome support trailing commas in functions? Does Firefox support the exponentiation operator?
I’ve found myself having to stop and look up all these things that I’ve been using for eons, and having to do a lot more cross-browser testing. In any reasonably sized app, this would quite likely result in bugs showing up in production.
bread.slice(): the optional chaining operator. I’ve only been using the magical
prop?.value for a month or so (since Create React App 3.3 supported it out of the box) and already it feels clumsy to work without it.
Caching was the biggest stumbling block for me. Actually, the fact that the resolution was interesting is the main reason I decided to write a blog post about modules.
As I’m sure you know, when you bundle your files with a bundler, each one will get a unique name like
index.38fd9e.js. The contents of the file with the name
index.38fd9e.js will never ever (ever) change, so it can be cached by the browser forever and never fetched over the network again.
It’s a wonderful system and has all but done away with the phrase “have you tried clearing cache”?
But when you load a module like
<script type="module" src="index.mjs"></script>, how is the browser supposed to know whether to fetch
index.mjs from cache or over the network, without that hash?
Well, it’s almost possible to get caching working acceptably, but it’s a lot of fiddling about. Here’s what you’ll need to do:
- Set the
cache-controlheader on all responses to
no-cache. Insanely, ‘no-cache’ does not mean no cache. It means: cache the file, but it must not “be used to satisfy a subsequent request without successful validation on the origin server”.
- Use ETags. If you don’t know, an ETag is (usually) a hash of the file contents that is sent as a header with a file — basically you’re moving
38fd9eout of the file name and into a header.
- Get a good CDN with bustable cache. The browser is going to be checking the ETags on all your files every time your site loads, and there will be a lot of them. So you want a fast CDN, and you’ll want to update its cache (with newly generated ETags) whenever you release a new version of the site. (This happens automagically with Firebase hosting.)
- Set up a service worker, acting as a ‘one-step-behind’ cache. It will intercept all requests and respond with whatever it already has in cache, and then update the cache from the network in the background.
So, when a repeat-visitor comes to your site, the browser will say “I need to load
index.mjs, I see I already have this file in cache, with the ETag
38fd9e. I will now ask the server for
index.mjs, but tell it to only send the file if its ETag isn’t
38fd9e.” The service worker will intercept this request, ignore the ETag, and return
index.mjs from its cache (from the last time the page loaded). The service worker will then forward the request on to the server. The server will respond with the file — or a message saying the file hasn’t changed — which then gets stored in cache.
I’ve been to productions of Fiddler on The Roof that were less fiddly than that.
The service worker, for the curious, looks something like this:
I was lazy and didn’t learn/use the hot new navigation preload feature, because by this point I’d spent more time on caching than I’d spent on writing my actual application (about 11 hours and 10 hours respectively, for the record).
Side note: the ‘import maps’ proposal would address some of this by allowing you to map
index.38fd9e.mjs. But you would still need a build pipeline to generate those hashes and inject the import map into your html file. Which means you need a bundler, which means you don’t need ES6 modules in the browser.
So, although all of this was an interesting learning experience, it was much like that year I travelled everywhere by unicycle. I shan’t be doing it again.
OK but not everyone uses a bundler, maybe?
I wrote this with the assumption that everyone writes their code in modules, with either
require, then uses something like Webpack/Grunt/Gulp/Spit to bundle it up for deployment.
I try and make all decisions like this based on the holy trinity of effective development: productivity and quality.
Unfortunately, modules in the browser don’t cut the mustard on either count.
I were to start a new project tomorrow, there’s no question in my mind that I would use good old create-react-app. I can be up and running in 30 seconds, and while the starting size of 40 KB is a little bit chubby, it’s no big deal for most sites.
It will be interesting to see what Webpack and friends do with ES6 modules in the browser. I imagine that as the years roll by — and ‘import maps’ get sufficient support — bundlers may use them as a way to abstract away those unsightly hashes.
Well, that’s just about all, folks. If you’re keen to dive a bit deeper into modules in the browser, this article from the V8 folks goes into the nuts and bolts.
Thanks for reading, have a super day!