ES6 modules in the browser: are they ready yet?

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:

index.html will load index.mjs, and index.mjs will load utils.mjs.

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?

It’s vanilla JavaScript! No build pipeline, no 400 line Webpack config, no Babel, no plugins and presets and 45 other npm modules to install. It’s just you and your code, no muss, no fuss.

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

OK so Vanilla JavaScript and no-build-config-required are some major benefits. What else, what else …

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
  • Super-modern JavaScript
  • Non-JavaScript syntax (React, TypeScript, etc.)
  • npm modules
  • Caching

Let’s look at three of the five in more detail:

File size

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 , 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 type="module", I thought that even though I’d be going code commando, I’d be targeting modern browsers, so would be able to use the modern JavaScript I’ve become accustomed to.

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.

It also means I can’t use the greatest addition to JavaScript since bread.slice(): the . 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 woes

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-control header on all responses to no-cache. Insanely, ‘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 38fd9e out 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 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 would address some of this by allowing you to map index.mjs to 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 export/import or require, then uses something like Webpack/Grunt/Gulp/Spit to bundle it up for deployment.

Are there people out there who don’t bundle? People who are writing JavaScript across multiple files and shipping it to production without any build step? Are you one of those people? I want to hear about your life. Tell me everything. Ev. Ree. Thing.


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.

On the other hand, If I was throwing together a bit of HTML/CSS and JavaScript for some sort of experiment, and it grew to more than a few files, and I didn’t want to spend time setting up a build pipeline, then I’d maybe use modules in the browser, if I didn’t care about performance.

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

Thanks for reading, have a super day!

I like web stuff.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store