TL;DR: see https://en.wikipedia.org/wiki/Betteridge%27s_law_of_headlines. Or just skip to the summary at the end of the post.

I have a dream! And in my dream, all the tooling needed today to use JavaScript just goes away. We can all just write the code in our favorite editor, hit refresh, and be done with it. No package.json . No babel . No webpack . No config.foo.json , or .foorc . Just write the code, and hit refresh.

Obviously, big applications will always need tooling — linters, static analysis, optimization tools for production. But even when coding big applications, while we’re just running our code in development, all we want to do is code and refresh.

One day I’ll write a blog post about how to get there, but today I want to try out one of the steps that will allow us to get there: ES Modules.

ES Modules

A quick recap on what ES modules are. There are better tutorials out there on this subject, so feel free to go and read them if you’re not familiar with ES modules. This recap is mostly to synchronize and understand it from the point of view of this article.

To create an ES module, we write code like this, which export -s a function.

And to import it, we use import .

As I said, this isn’t really a tutorial, and to really explore the syntax, I would suggest you go to this tutorial. What is important to me, is that if, today, we script src the module above that imports the other module, then it won’t work on any browser — the browser will treat the import syntax as a syntax error. Browser vendors say that their latest versions have more or less 100% coverage of ES6, and the wonderful kangax compatibility table agrees, but that is because, for some reason, ES modules aren’t included in their definition of “ES6”, even though it is part of that version.

But all the cool kids use ES Modules today. How? How can most React and Angular samples use ES Modules? Because, as is usual in these times, the JavaScript community has found ways of circumventing browser vendors, and enabling ES Modules using tools like Webpack, browserify, and rollup.

These tools are called module bundlers, since they crawl the JavaScript modules looking for import statements, and bundle all the modules into one big file.

CommonJS and ES modules, along with npm, have revolutionized the way we write JavaScript, and it would be inconceivable today to write a software project without using them.

Unfortunately, no browser supported has supported CommonJS or ES modules. Till now.

Native ES Modules

Starting from the last week, all 4 major browsers — Safari, Firefox, Edge, and Chrome — have started supporting ES modules natively. This means that if you script src the module that imports, it will JUST WORK. Well, it will “just work” currently only in the “developer/canary/technology preview/insider” versions.

My dream is coming closer! But I was skeptical. All the examples I’ve seen in the various blog posts try and do 2–3 modules importing one another. But the reality today is that an application can bring in ten or even hundreds of modules at a time — each one very small, true, but hundreds of them nonetheless.

So I decided to try and see how those browsers work when there are tens of modules being imported. I wanted to check out how browsers would behave when confronted with lots of little modules?

Trying Out Native ES Modules

Luckily, I didn’t have to build a module that imports tens of other modules: there is such a module already. Moreover — it’s a VERY famous module. It’s lodash. Most people don’t know, but for some reason, the lodash team have a package in npm that has lodash importing the other sub-lodash modules (e.g. pick and map and compose and debounce and others) using ES modules syntax. And there a lot of sub-lodash modules.

So all I needed to do, was create an HTML page that had a script tag that imports lodash, and try it out. Does it work? Is the performance of loading multiple modules, using multiple HTTP requests, comparable to loading one big bundle?

I was even all ready to try this out using HTTP/2, as HTTP/2 is MUCH better at handling multiple small resources from the same domain. And then to test how HTTP/2 push support will make it even better. As we shall see, I didn’t get there, because, well… you’ll see.

You can try out for yourself the results of my examination here:

To try them out, you will need to run npm install , then npm run build , and finally npm start to start the node server that serves the HTML and JS. If you go to http://localhost:3000/es6-modules.html you will get a page that tries to import lodash, and console.log -s the result:

The code loads es6-module-1.js . This is the module that loads lodash , which in turns loads all the little lodash sub-modules.

So let’s try it on Chrome Canary. If you also want to try it, you currently need to goto chrome://flags and enable “Experimental Web Platform features”.

Native ES Modules on Chrome Canary (v60)

So I tried it out. Imagine my amazement when I got this result:

benchmark: 18484.576904296875ms

Yup — loading all those modules took more than 18 seconds.

If we try out the link in that page — webpack modules —we will get:

benchmark: 191.23876953125ms

And this page loads the same code as the previous page, but bundled into one file. Oh, and the file isn’t that big — around 1Kb. But it runs in around 200 milliseconds. Much lower than 18 seconds.

So what’s going on? Why is Chrome so slow in loading ES modules? Is it the loads of HTTP requests? Would HTTP/2 help here, by parallelizing all HTTP request? No. I made all requests to the server return an infinity expiration date (using the maxage HTTP directive), and tried again — same result.

So if the browser isn’t doing any HTTP requests when caching is turned on, then what is going on? What is it doing? According to the Chrome’s Network tab, the JS files were loaded (when cached) after 3 seconds (which is still a lot). But what is it doing for 15 seconds after that?

I tried figuring out what it was doing using the “Performance” tab:

I found out that after about 6 seconds, it’s basically doing nothing till the 20th second — those little yellow lines spread around are just DOM Garbage Collection.

So what is making it run so slow? No idea. It’s not the network problem, as we have proven by turning on caching. but I tried HTTP/2 anyway using https://localhost:3001/es6-modules.html . Same result! the browser is now loading the modules in parallel, but the time is still around the 20 seconds.

This was very disappointing — what I really wanted to do was optimize HTTP/2 fetching so that loading the modules would be almost as fast as loading the bundles, but with this performance, there’s no point in shaving off the network time, because the browser is doing something else after loading the JS modules.

But now I got curious. What about the other browsers?

Native ES Modules on Firefox Developer Edition (v54)

Mostly, the same result:

benchmark: 11749.47ms

Oh, wait, no. It’s actually worse. I ran it a few more times, and consistently got numbers like these:

benchmark: 31409.71ms

Moreover, the browser seems to freeze when loading the page! The performance is much worse than in Chrome.

What about Safari?

Native ES Modules on Safari Technology Preview (v29)

Much better! Without caching, the result I get is:

benchmark: 2826.892ms

And with caching:

benchmark: 1583.700ms

Of course, this is not even comparable to the performance of the bundled version, which takes 200ms uncached, and 40ms cached. But it’s a huge improvement over Chrome and Firefox. This reminds of the story of the poor family, the rabbi, and the goat. Safari got rid of the goat.

And it still begs the same question — what is Safari doing for 1.5 seconds if all the files are in the cache?

Summary

Chrome, Firefox, and to a lesser extent, Safari, all exhibit the same behavior when loading ES Modules natively — something makes the browsers idle for a long time after the network loading of the modules is done. And that something is making the loading time of a page that uses lots of ES modules very high: around 18 seconds for Chrome, more than 30 seconds for Firefox, and around 2 seconds for Safari.

I couldn’t figure out what that something is. Maybe somebody can refer this blog post to the various browser developers? I’d love to understand what it is!

One thing is for certain — I am absolutely sure that this is a temporary thing, and that the browser developers will optimize this. And when they do, I will go on and optimize the network part using HTTP/2 and other technologies.

But that’s for another blog post.

Update

People have been urging me to create bug reports, so here they are: