Shipping ES6 in browsers without Polyfill

Arnav Yagnik
OYOTech
Published in
5 min readApr 17, 2019

In today’s world and age as web developers, we understand that there is a need to ship minimal JavaScript to a user’s browser. So, we try to reduce our scripts’ sizes by implementing various techniques(code splitting, compression, and whatnot) for our users. But being developers we want to use the latest JavaScript syntax (arrow functions, classes, const, let, async/await, etc..) which browsers can’t understand yet. Hence we include the polyfills, contrary to our intention to ship less JavaScript.

However, browsers are evolving and accepting our newer ECMAScript syntax and supporting them directly, so shipping polyfill for them is just redundant. But, there is still a need to ship the polyfills since our users might not use an upgraded browser.

How did we solve this: ES Modules

JavaScript Module or ES Module is JavaScript code that can execute in isolation or can be called as a library. We use this every day in our project as

import myFunc from “my-library”

We write our JavaScript code in classes now which can be later imported across our codebase, what if we tell you that the files can be shipped to a browser and assure you that they will work. How? The answer is :

<script type=”module”>

When you declare type=module attribute on a script it means :

  1. It tells the browser to load JavaScript Modules.
  2. Whichever browser understands module, will also understand all the es6 syntax and can parse it.
Support for Es Modules according to caniuse

This means that ~84% of global users now support modules. That is what convinced us @OYO to leverage the opportunity here.

Implementation

We use webpack as module bundler. So next, we will learn how to generate bundles which will not include the es5 transpiled code and polyfills.
But we would not want to ignore the rest 16% users, so we would generate 2 builds, let’s call them modern and legacy builds.

Note: Steps are for webpack based project, If you use any other bundler, please read respective documents on how to achieve the same effect as below.

Let’s start :

  1. Generating 2 Builds: You have to run webpack bundling twice, webpack accept an array of configurations, which will do the job for us.
Config includes all the polyfills and modernConfig doesn’t
  1. Generating mjs files: You would want to change all test regex in rules to accept mjs and output to generate files as .mjs
test: /\.(js|jsx)$/ ==> test: /\.(js|jsx|mjs)$/

3. Changing Babel configuration: You might have a babel configuration setup either in .babelrc or in your config. If you have a .babelrc, you need to delete it and use `@babel/preset-env` npm package.

4. Shipping the builds in your HTML: Now this is where it gets tricky, the ideal way is to just include 2 script tags for every script you want to load.

<script type="module" src="main.modern.mjs"></script>
<script nomodule src="main.legacy.js"></script>`

But this doesn’t work for every use case such as :

  1. IE/Edge/Safari(10.1/3) will not execute type=”module” scripts but they will still download it which is just bad.
  2. Edge will download the type=”module” files twice and won’t be able to execute, which is even worse.

So, after much thought, we decided to ship it to our mobile web users as the user base on the above-mentioned browsers was comparatively low and even then we did this with user agent sniffing, where we only ship our modern build when the browser is known to support mjs builds. The Sweet spot for us was chrome 71+

So, at our server, we detect what browser is requesting the webpage and serve the HTML accordingly by our in-house HTML generation logic. Now, if you are using some plugin to generate HTML like `webpack-html-plugin`, you might want to generate two HTML templates and serve them accordingly.

Tips:

  1. If your project uses uglify-js to obfuscate the code, you should change that in favor of Terser, as uglify-js doesn’t behave well with mjs files.
  2. Mjs files must be served as JavaScript files. For nginx in mime.types :
types {
text/javascript mjs js;
}

3. If you use some plugin to generate assetManifest. `assets-webpack-plugin` package might come handy. Their documentation explains how to generate manifests in multi-compiler mode.

4. You would want to cache the mjs files at your cdn and service worker.

5. All polyfills are not supported by browsers, so you should include them in babel configuration, If you use them :

plugins: ['@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-class-properties']

Benefits :

  1. We at OYO Engineering & Data Science are observing 50KB(gzipped) savings which have translated to 11% decrease in JavaScript size on our consumer’s website.
  2. Smaller scripts are faster to parse and evaluate which is an expensive javascript operation performed before TTI. Following are the pre-post results :
A performance summary from Chrome’s DevTools showing reduced JavaScript activity.

3. Users are upgrading to newer browsers every day and browsers are adopting ES6 syntax. Hence, migration to MJS is bound to scale up your benefits in the near future.

Let me know if this was helpful. Cheers!

Responses (2)

What are your thoughts?