How to Deploy ES2015+ JavaScript in Production Without Unnecessary Polyfills

This article explains how modern browsers that support ES modules can load native ES2015+ JavaScript while older browsers receive a compiled ES5 fallback, detailing conditional polyfill loading, webpack and Babel configurations, script tag usage, performance benefits, and practical implementation steps.

JD.com Experience Design Center
JD.com Experience Design Center
JD.com Experience Design Center
How to Deploy ES2015+ JavaScript in Production Without Unnecessary Polyfills

Most frontend developers love using new JavaScript features such as async/await, classes, and arrow functions. Although all modern browsers can run ES2015+ code, many still transpile to ES5 with polyfills to support older browsers.

This situation is sub‑optimal; ideally we would only send the code that each browser can execute.

By using feature detection together with the <script type=\"module\"> tag we can conditionally load polyfills, but new syntax can cause parsing errors in browsers that do not understand it, making detection tricky.

Fortunately, browsers that support <script type=\"module\"> also support most ES2015+ syntax (async, classes, arrow functions, fetch, promises, Map, Set, etc.). The only thing we need to provide for non‑supporting browsers is a fallback bundle.

Implementation

If you already use a bundler such as Webpack or Rollup, keep your existing configuration and add a second configuration that outputs an ES2015+ bundle without transpiling to ES5 or adding polyfills.

When using babel-preset-env, configure it to target browsers that support modules, so Babel will skip unnecessary transformations.

Example Webpack configuration for the legacy (ES5) bundle:

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'] }
            }
          ]]
        }
      }
    }]
  }
};

Configuration for the modern (ES2015+) bundle:

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'] }
            }
          ]]
        }
      }
    }]
  }
};

After building, you will have two files:

main.js (ES2015+)

main-legacy.js (ES5)

In your HTML, load the appropriate bundle conditionally:

<!-- Browsers with ES module support load this file -->
<script type=\"module\" src=\"main.js\"></script>

<!-- Older browsers load the legacy file -->
<script nomodule src=\"main-legacy.js\"></script>

Note that Safari 10 does not support the nomodule attribute; a small inline script can work around this.

Considerations

Modules are loaded with defer semantics, so code that must run earlier should be split.

Modules always execute in strict mode; non‑strict code must be loaded separately.

Global var and function declarations behave differently inside modules.

Example Project

The author provides a webpack-esnext-boilerplate that demonstrates code‑splitting, dynamic imports, and asset fingerprinting.

Performance Impact

Version

Size (minified)

Size (minified + gzipped)

ES2015+ (main.js)

80 KB

21 KB

ES5 (main-legacy.js)

175 KB

43 KB

Version

Parse/eval time (individual runs)

Parse/eval time (avg)

ES2015+ (main.js)

184 ms, 164 ms, 166 ms

172 ms

ES5 (main-legacy.js)

389 ms, 351 ms, 360 ms

367 ms

Even on a modest device the ES2015+ bundle parses and executes roughly half as fast as the ES5 fallback, and its size is less than a third after gzip.

Widespread use of polyfills inflates bundle size; data from HTTPArchive shows a rapid increase in sites that include babel-polyfill, core-js, or regenerator-runtime.

Conclusion

Using <script type=\"module\"> together with nomodule provides a practical way to ship native ES2015+ code to modern browsers while still supporting legacy ones, reducing unnecessary bytes and improving performance. Publishing ES2015+ modules to npm is now feasible and beneficial for both developers and users.

Further Reading

ES6 Modules in Chrome M61+

ECMAScript modules in browsers

ES6 Modules in Depth

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

babelwebpackES2015Polyfills
JD.com Experience Design Center
Written by

JD.com Experience Design Center

Professional, creative, passionate about design. The JD.com User Experience Design Department is committed to creating better e-commerce shopping experiences.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.