• ES6 Modules Support lands in Browsers: Is it time to rethink bundling?
  • By Stefan Judis
  • The Nuggets translation Project
  • Translator: lsvih
  • Proofread by: Aladdin-ADD, Yzgyyang

As ES6 module native support hits the browser, is it time to rethink packaging?

Writing efficient JavaScript applications has become increasingly complex these days. A few years ago, everyone started combining scripts to reduce the number of HTTP requests; With the advent of compression tools, variable names were shortened in order to compress code, even saving the last byte of code.

Today, with Tree Shaking and a variety of module packagers, we’re back to code splitting to speed up interaction times so that we don’t block the main process during the first screen load. We also started translating everything: thanks to Babel, we were able to use future features now.

The ES6 module has been finalized for some time by the ECMAScript standard. The community has written a number of articles about how to use them with Babel and the difference between import and Node.js require. But it will take a while to actually implement it in the browser. I was pleasantly surprised to find that Safari is the first to ship the ES6 module in its Technology Preview release, and that THE Edge and Firefox Nightly editions will also support the ES6 module — though not yet. After using tools like RequireJS and Browserify (remember the discussion about AMD vs. CommonJS?) At least it looks like browsers will finally support modules. Let’s take a look at the gift of a bright future. 🎉

The traditional method

A common way to build Web applications is to use bundles built by Browserify, Rollup, Webpack, etc. Sites that do not use SPA (single page application) technology typically have HTML generated by the server, with a JavaScript code package in it.

<html>
  <head>
    <title>ES6 modules tryout</title>
    <! -- defer to not block rendering -->
    <script src="dist/bundle.js" defer></script>
  </head>
  <body>
    <! -... -->
  </body>
</html>Copy the code

Our code package with Webpack includes three JavaScript files that use the ES6 module:

// app/index.js
import dep1 from './dep-1';

function getComponent () {
  var element = document.createElement('div');
  element.innerHTML = dep1();
  return element;
}

document.body.appendChild(getComponent());

// app/dep-1.js
import dep2 from './dep-2';

export default function() {
  return dep2();
}

// app/dep-2.js
export default function() {
  return 'Hello World, dependencies loaded! ';
}Copy the code

The app will display “Hello World”. If “Hello World” is displayed, the script is loaded successfully.

Loading a bundle

Configuring to create a code package using Webpack is relatively straightforward. During the build process, nothing is done other than packaging and compressing JavaScript files using UglifyJS.

// webpack.config.js

const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  entry: './app/index.js'.output: {
    filename: 'bundle.js'.path: path.resolve(__dirname, 'dist')},plugins: [
    new UglifyJSPlugin()
  ]
};Copy the code

The three base files are relatively small, adding up to only 347 bytes.

$ ll app
total 24
-rw-r--r--  1 stefanjudis  staff    75B Mar 16 19:33 dep1.js
-rw-r--r--  1 stefanjudis  staff    75B Mar  7 21:56 dep- 2.js
-rw-r--r--  1 stefanjudis  staff   197B Mar 16 19:33 index.jsCopy the code

After I built with Webpack, I got an 856-byte code package, about 500 bytes bigger. These extra bytes are still acceptable, and the package is no different from what we would normally do in a production environment. Thanks to Webpack, we are ready to use the ES6 module.

$ webpack
Hash: 4a237b1d69f142c78884
Version: webpack 2.21.
Time: 114ms
Asset       Size        Chunks  Chunk Names
bundle.js   856 bytes   0       [emitted]  main
  [0] ./app/dep1.js 78 bytes {0}[built]
  [1] ./app/dep- 2.js 75 bytes {0}[built]
  [2] ./app/index.js 202 bytes {0}[built]Copy the code

New Settings for ES6 modules using native support

Now we have a “traditional packaged code” that is now supported by all browsers that do not yet support ES6 modules. We can start playing with some fun things. Let’s add a new script element to the index.html to point to the ES6 module and add type=”module” to it.

<html><head><title>ES6 modules tryout</title><! -- in case ES6 modules are supported --><script src="app/index.js"type="module"></script><script src="dist/bundle.js"defer></script></head><body><! -... --></body></html>Copy the code

Then we look in Chrome and see that nothing is happening.

The code package loads as before, “Hello World!” It also displays normally. It doesn’t work, but it’s nice to see that browsers can accept commands they don’t understand without reporting errors. Chrome ignores the script element, whose type it cannot determine.

Next, let’s try Safari Technology Preview:

Unfortunately, it doesn’t show another “Hello World.” The problem is caused by the difference between the build tool and the native ES module: Webpack finds the files that need to be included during the build process, whereas the ES module gets the files while running in the browser, so we need to specify the correct file path for this:

// app/index.js

// Do not write this way
// import dep1 from './dep-1';

// This will work
import dep1 from './dep-1.js';Copy the code

It worked fine after changing the file path, but the fact that Safari Preview loaded the code package, along with three separate modules, meant that our code was executed twice.

The solution to this problem is to add the nomodule attribute, which can be added to the script element that loads the package. This property was recently added to the standard, and Safari Preview only supported it at the end of January. This property tells Safari that this script is a “fallback” when ES6 modules are not supported. In this case, the browser supports ES6 modules so the code in the script element with this attribute will not execute.

<html>
  <head>
    <title>ES6 modules tryout</title>
    <! -- in case ES6 modules are supported -->
    <script src="app/index.js" type="module"></script>
    <! -- in case ES6 modules aren't supported -->
    <script src="dist/bundle.js" defer nomodule></script>
  </head>
  <body>
    <! -... -->
  </body>
</html>Copy the code

It’s all right now. Using a combination of type=”module” and nomodule, we can now load traditional code packages in browsers that do not support ES6 modules and JavaScript modules in browsers that do.

You can check out the spec in progress at es-module-on.stefan-playground.rocks.

Differences between modules and scripts

There are a couple of questions here. First, JavaScript runs differently in ES6 modules than it normally does in script elements. Axel Rauschmayer has a good discussion of this issue in his book Exploring ES6. I recommend you click on the link above to read the book, but let me quickly summarize the main differences:

  • ES6 modules run in strict mode by default (so you don’t need to adduse strictA).
  • The outermost layer of thethisPoint to theundefined(not Window).
  • The highest level variable is local to module (not global).
  • The ES6 module loads and executes asynchronously after the browser has finished parsing the HTML.

I think these features are great progress. Modules are local — which means we no longer need to use IIFE everywhere, and we don’t have to worry about global variables leaking. And the default runs in strict mode, which means we can discard the Use Strict declaration in many places.

The IIFE full name was the immediate-invoked function expression, but the function was invoked immediately.

From the point of view of improving performance (and perhaps the most important advance), modules load and execute lazily by default. As a result, we will no longer accidentally add loading-blocking code to our site, and script elements using type=”module” will no longer have SPOF problems. We can also add an async property to it, which will override the default lazy loading behavior. However, using defer is also a good option for now.

SPOF Single Points Of Failure

<! -- not blockingwith defer default behavior -->
<script src="app/index.js" type="module"></script><! -- executed after HTML is parsed --><script type="module">
  console.log('js module');
</script><! -- executed immediately --><script>
  console.log('standard module');
</script>Copy the code

If you want to learn more about this, read the script element description, which is easy to read and includes some examples.

Compress pure ES6 code

Yet!!! We can now provide a zipped code package for Chrome, but not a zipped file for Safari Preview. How can we make these files smaller? Is UglifyJS up to the task?

It must be noted, however, that UglifyJS does not fully handle ES6 code. Although it has a Harmony development branch (address) that supports ES6, unfortunately it didn’t work when I wrote these 3 JavaScript files.

$ uglifyjs dep1.js -o dep1.min.js
Parse error at dep1.js:3.23
export default function() {^SyntaxError: Unexpected token: punc (()
// ..
FAIL: 1Copy the code

But now that UglifyJS is in almost every toolchain, what about projects written entirely in ES6?

The usual process is to use a tool like Babel to convert the code to ES5, and then use Uglify to compress the ES5 code. But I don’t want to use the ES5 translation tool for this article, because we’re looking for a way to deal with the future! Chrome already covers 97% of the ES6 specification, and Safari Preview supports ES6 100% well since Verion 10.

I asked on Twitter if there was a compression tool that could handle ES6, and Lars Graubner told me Babili was available. Using Babili, we can easily compress ES6 modules.

// app/dep-2.js

export default function() {
  return 'Hello World. dependencies loaded.';
}

// dist/modules/dep-2.js
export default function(){return 'Hello World. dependencies loaded.'}Copy the code

Using the Babili CLI tool, you can easily compress individual files individually.

$ babili app -d dist/modules
app/dep1.js -> dist/modules/dep1.js
app/dep- 2.js -> dist/modules/dep- 2.js
app/index.js -> dist/modules/index.jsCopy the code

Final result:

$ ll dist
-rw-r--r--  1 stefanjudis  staff   856B Mar 16 22:32 bundle.js

$ ll dist/modules
-rw-r--r--  1 stefanjudis  staff    69B Mar 16 22:32 dep1.js
-rw-r--r--  1 stefanjudis  staff    68B Mar 16 22:32 dep- 2.js
-rw-r--r--  1 stefanjudis  staff   161B Mar 16 22:32 index.jsCopy the code

The code package is still about 850B, and all the files add up to about 300B. I didn’t use GZIP because it doesn’t handle small files very well. (We’ll get to that later.)

Can you speed up ES6 module loading with rel=preload?

The compression of a single JS file has achieved good results. The file size was reduced from 856B to 298B, but we were able to speed up the loading even further. By using the ES6 module, we can load less code, but if you look at the waterfall diagram, requests are loaded consecutively, one by one, along the dependency chain of the module.

If we told the browser to load additional requests with elements like we did in the browser before we preloaded the code, would that speed up module loading? We already have similar tools in Webpack, such as Addy Osmani’s Webpack preloading plugin for pre-loading split code, but is there a similar approach for ES6 modules? If you’re not sure how rel=” Preload “works, you can start by reading Yoav Weiss’s related article at Smashing Magazine: Click to Read

However, preloading ES6 modules is not that simple, and they are quite different from normal scripts. So what happens to ES6 modules when you add rel=”preload” to a link element? Does it also pull out all dependent files? This problem is obvious (and it can be), but loading modules with the preload command requires more internal browser implementation issues. Domenic Denicola discusses this issue in a GitHub issue that you can check out if you’re interested. But it turns out that loading a script with rel=”preload” is very different from loading an ES6 module. Perhaps the ultimate solution later would be to use another rel=”modulepreload” command to load modules specifically. The Pull Request is still under review as of this writing, so you can click on it to see how we might preload modules in the future.

Add real dependencies

Of course you can’t make a real app with just 3 files, so let’s add some real dependencies to it. Lodash has segmented its functionality based on ES6 modules and offers it to users separately. I took one of the functions and compressed it using Babili. Now let’s modify the index.js file to introduce the Lodash method.

import dep1 from './dep-1.js';
import isEmpty from './lodash/isEmpty.js';

function getComponent() {
  const element = document.createElement('div');
  element.innerHTML = dep1() + ' ' + isEmpty([]);

  return element;
}

document.body.appendChild(getComponent());Copy the code

In this example, isEmpty is basically unused, but after adding its dependencies, we can see what happens:

You can see the number of requests increase to over 40, the page load time on normal wifi increases from around 100 milliseconds to between 400 and 800 milliseconds, and the total size of the loaded data increases to around 12KB without compression. Unfortunately, WebPagetest is not available in Safari Preview, and we can’t do a reliable standard check for it.

However, Chrome received a relatively small amount of packaged JavaScript data, about 8KB.

This 4KB gap is not to be ignored. You can find this example at lodash-module-on.stefan-playground.rocks.

Compression works well only for large files

If you look closely at the screenshot of the Safari developer tool above, you may notice that the transferred file size is actually larger than the source code. This is especially true in large JavaScript apps, where a bunch of small chunks can make a big difference in file size because GZIP doesn’t shrink files very well.

Khan Academy explored the same problem a while back, using HTTP/2. Loading smaller files is a good way to ensure cache hit ratios, but in the end it is generally a trade-off, and its effectiveness can be affected by many factors. Splitting a large code base into chunks (a vendor file and an app bundle) is natural, but loading thousands of small files that can’t be compressed is probably not a sensible approach.

Tree Shaking is a COOL technology

It must be said: Thanks to the very new Tree Shaking technology, the build process can remove code that is not used or referenced by other modules. The first build tool to support this technique was Rollup, and Webpack 2 now supports it as well — as long as we disable the Module option in Babel.

We tried to modify dep-2.js to include things that would not be used in dep-1.js.

export default function() {
  return 'Hello World. dependencies loaded.';
}

export const unneededStuff = [
  'unneeded stuff'
];Copy the code

Babili only compresses the file, and Safari Preview receives the unused lines in this case. On the other hand, Webpack or Rollup packages will not contain this unnededStuff. Tree Shaking omits a lot of code, and it should definitely be used in real production code bases.

While the future is clear, the current building process remains the same

The ES6 module is coming, but until it is finally implemented in major browsers, nothing will change in our development. We don’t load a bunch of small files to ensure compression, nor do we throw away the build process for tree shaking and dead code removal. Front-end development is and always will be complex.

Don’t slice everything up and assume it will improve performance. We’re on the verge of native browser support for ES6 modules, but that doesn’t mean we can throw away the build process and a proper packaging strategy. Here at Contentful, we’ll stick with our build process and continue to use our JavaScript SDKs for packaging.

However, we have to admit that the front-end development experience is still good right now. JavaScript is still evolving, and eventually we’ll be able to use the module system provided by the language itself. What will be the impact of native modules on the JavaScript ecosystem and best practices in a few years? Let’s wait and see.

Other resources

  • ES6 module series by Serg Hospodarets
  • Explore the module chapter of ES6

The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. Android, iOS, React, front end, back end, product, design, etc. Keep an eye on the Nuggets Translation project for more quality translations.