Most of the web developers I’m chatting with now love writing JavaScript with all the latest language features — async / await, classes, switch functions, etc. However, despite the fact that all modern browsers can execute ES2015 + code and initially support the functionality mentioned by me, most developers still transfer their code to ES5 and associate it with polyfills in order to satisfy a small percentage of users still working in old browsers.
It's disgusting. In a perfect world, we will not deploy unnecessary code.

When working with new APIs, JavaScript and DOM, we can
conditionally load polyfills , because we can identify support for these interfaces during program execution. But with the new JavaScript syntax, this is much more difficult to do, since any unknown syntax will cause a parse error, and then our code will not run at all.
')
Although we currently do not have a suitable solution for creating a new syntax detection functionality, we have a way to install basic support for the ES2015 syntax today.
Let's solve this with the help of the
script type="module"
.
Most developers think of
script type="module"
as a way to load ES modules (and, of course, it is), but
script type="module"
also has a faster and more practical use case — it loads regular JavaScript files with ES2015 + functions, knowing that the browser can handle them!
In other words, every browser that supports
script type="module"
also supports most of the ES2015 + functions that you know and love. For example:
- Each browser supporting
script type="module"
also supports async / await - Every browser that supports
script type="module"
also supports classes . - Every browser that supports
script type="module"
also supports dialing functions . - Every browser that supports
script type="module"
also supports fetch , Promises , Map , Set , and more!
It remains only to provide a backup for browsers that do not support
script type="module"
. Fortunately, if you are currently generating an ES5 version of your code, you have already done this work. All you need now is to create a version of ES2015 +!
The rest of this article explains how to implement this technique and discusses how the ability to deploy ES2015 + code will change the way modules are created in the future.
Implementation
If you are already using a module bundler, such as a webpack or rollup, to generate your JavaScript code, continue to do so.
Then, in addition to your current bundle, you will create a second set, like the first; the only difference will be that you will not translate the code in ES5, and you will not need to connect legacy polyfills.
If you are already using
babel-preset-env
(which should), then the second step will be very simple. All you need to do is change the list of browsers only to those that support
script type="module"
, and Babel will not automatically make unnecessary conversions.
In other words, this will be the output of the ES2015 + code instead of ES5.
For example, if you use webpack, and your main entry point is the script
./path/to/main.js
, then the configuration of your current version of ES5 might look like this (note, since this is ES5, I call the bundle)
main-legacy
):
module.exports = { entry: { 'main-legacy': './path/to/main.js', }, output: { filename: '[name].js', path: path.resolve(__dirname, 'public'), }, module: { rules: [{ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['env', { modules: false, useBuiltIns: true, targets: { browsers: [ '> 1%', 'last 2 versions', 'Firefox ESR', ], }, }], ], }, }, }], }, };
In order to make a modern version for ES2015 +, all you need is to create a second configuration and configure the target environment only for browsers that support
script type="module"
. Here is what it might look like:
module.exports = { entry: { 'main': './path/to/main.js', }, output: { filename: '[name].js', path: path.resolve(__dirname, 'public'), }, module: { rules: [{ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['env', { modules: false, useBuiltIns: true, targets: { browsers: [ 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15', ], }, }], ], }, }, }], }, };
When running, these two configurations will output two JavaScript files to the production:
main.js
(ES2015 + syntax)main-legacy.js
(ES5 syntax)
The next step is to update your HTML for conditional download of the ES2015 + bundle in browsers that support the modules. You can do this using
script type="module"
and
script nomodule
:
<script type="module" src="main.js"></script> <script nomodule src="main-legacy.js"></script>
Attention! The only gotcha here is Safari 10, which does not support the
nomodule
attribute, but you can solve this by
embedding the JavaScript snippet in your HTML before using any
script nomodule
tags. (
Note: this was fixed in Safari 11 ).
Important points
For the most part, the above technique “just works”, but before its implementation it is important to know a few details about how modules are loaded:
- Modules are loaded as
script defer
. This means that they are not executed until the document is parsed. If some part of your code needs to be run earlier, it is better to break this code and load it separately. - Modules always run code in strict mode , so if for some reason a part of your code needs to be run outside of strict mode, you will have to download it separately.
- Modules handle the top-level declarations of variables (
var
) and functions ( function
) different from normal scripts. For example, var foo = 'bar'
and function foo() {…}
in the script can be accessed through window.foo
, but in the module it will not work. Make sure that your code does not depend on this behavior.
Working example
I created a
webpack-esnext-boilerplate so that developers can see the real use of the technique described here.
In this example, I intentionally included several advanced features of the webpack, because I wanted to show that the technique I described is ready for use and works in real-world scenarios. These include well-known best practices, such as:
And since I will never recommend something that I do not use myself, I updated my blog to use this technique. You can check the
source code if you want to see more.
If you use a tool other than a webpack to create bundles for production, this process is more or less the same. I decided to demonstrate the technique I described with the help of a webpack, since at the present time it is the most popular tool for assembly, as well as the most complex. I suppose that if the technique described by me can work with a webpack, it can work with anything.
Is the game worth the candle?
In my opinion, definitely! Savings can be significant. For example, the following is a comparison of total file sizes for two versions of the code from my blog:
Version | Size (minified) | Size (minified + gzipped) |
---|
ES2015 + (main.js) | 80K | 21K |
ES5 (main-legacy.js) | 175K | 43K |
The outdated ES5 version of the code is more than twice the size (even gzipped) of ES2015 +.
Large files take longer to load, but they also take longer to analyze and evaluate. When comparing the two versions of my blog, the time spent on parse / eval was also stable twice as long for the outdated ES5 version (these tests were performed on Moto G4 using
webpagetest.org ):
Version | Parse / eval time (separately) | Parse / eval time (average) |
---|
ES2015 + (main.js) | 184ms, 164ms, 166ms | 172ms |
ES5 (main-legacy.js) | 389ms, 351ms, 360ms | 367ms |
Although these absolute file sizes and parse / eval times are not particularly large, understand that this is a blog and I don’t load a lot of scripts. But for most sites this is not the case. The more scripts you have, the more will be the gain that you get by deploying the code for ES2015 + in your project.
If you are still skeptical, and consider that file size and run-time differences are primarily related to the fact that you need a lot of polyfills to support legacy environments, you are not completely mistaken. But, for better or for worse, on the Internet today is a very common practice.
A quick
request for HTTPArchive data shows that among the best sites rated by Alexa,
85,181 include
babel-polyfill ,
core-js or
regenerator-runtime in their bundles production. Six months ago their number was 34,588!
Reality is transported, and including polyfills are quickly becoming the new norm. Unfortunately, this means that billions of users receive trillions of bytes, without having to be sent over the network to browsers that would initially be perfectly capable of running non-transposed code.
It's time to build our modules as ES2015
The main ambush (gotcha) for the technology described here now consists in the fact that most module authors do not publish the ES2015 + version of the source code, but publish the immediately transpiled ES5 version.
Now that the deployment of code on ES2015 + may be, it's time to change this.
I fully realize that such a step is fraught with many problems in the near future. Today, most build tools publish documentation
recommending a configuration that
assumes that all modules are written in ES5. This means that if the authors of the modules start publishing the source code on ES2015 + in npm, they will probably
break some user builds and simply cause confusion.
The problem is that most of the developers using Babel configure it so that the code in the
node_modules
not
node_modules
. However, if the modules are published with source code ES2015 +, a problem arises. Fortunately, it is easily fixable. You just need to remove the
node_modules
exception from the build configuration:
rules: [ { test: /\.js$/, exclude: /node_modules/,
The disadvantage is that if tools like Babel should start to transport dependencies (dependencies) from
node_modules
, in addition to local dependencies, this will slow down the build speed. Fortunately, this problem can be partially solved
at the toolbox level with constant local caching.
Despite the strikes, we are likely to go the way to the fact that ES2015 + will become a new standard for publishing modules. I think the fight is worth its goal. If we, as the authors of the modules, publish in npm only the ES5 version of our code, we impose bloated and slow code on users.
By publishing the code in ES2015, we give developers a choice that ultimately benefits everyone.
Conclusion
Although
script type="module"
designed to load ES modules (and their dependencies) in the browser, it does not need to be used
only for this purpose.
script type="module"
will successfully load a single Javascript file, and this will give developers a much needed means for conditional loading of modern functionality in those browsers that can support it.
This, along with the
nomodule
attribute, allows us to use ES2015 + code in production, and finally we can stop sending the transpiled code to browsers that do not need it.
Writing ES2015 code is a win for developers, and implementing ES2015 code is a win for users.