📜 ⬆️ ⬇️

Deploying ES2015 + code in production today

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:


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:


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 :

 <!--     ES   . --> <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:

  1. 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.
  2. 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.
  3. 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:
VersionSize (minified)Size (minified + gzipped)
ES2015 + (main.js)80K21K
ES5 (main-legacy.js)175K43K

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 ):
VersionParse / eval time (separately)Parse / eval time (average)
ES2015 + (main.js)184ms, 164ms, 166ms172ms
ES5 (main-legacy.js)389ms, 351ms, 360ms367ms

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/, //    use: { loader: 'babel-loader', options: { presets: ['env'] } } } ] 

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.

Source: https://habr.com/ru/post/338612/


All Articles