HUMAN is Named a Leader and Earns Top Scores in Nine Criteria in the Forrester Wave™: Bot Management Software, Q3 2024
Tech & Engineering Blog

What you don’t know about BabelJS preset-env

The TL;DR

Whether you are a full stack developer or a JavaScript developer, you should look into your preset-env configuration and optimize it to the browsers your customers actually use, and optimize bundle size to configure core-js to your needs. This will allow you to share a concrete set of supported browsers for your app while leveraging the power of the latest JavaScript features.

Background

Here at HUMAN, we maintain a third party JS script as part of our Bot Defender solution.

If you’ve ever worked on any third party script, you know the complexity of maintaining a script that runs on your customer’s websites, on millions of different browsers and a wide variety of platforms. So, when I took on the task of updating our build system’s libraries and moving BabelJS from preset-2015 to preset-env, I was very cautious. I found a lot of interesting stuff under the hood of babelJS and gathered insights on how it works with preset-env.

What did the yearly presets do exactly

The Babel yearly presets — preset-2015, 2016, etc. — were focused on converting ES6 JavaScript features to ES5. This allowed developers to write JavaScript code with the latest JavaScript features, such as promises, let and const, and arrow functions, according to their stage of approval.

Each yearly preset added more and more features. Any other features had to be added as extra polyfill libraries. Babel relied on the babel-polyfill library for the preset polyfills.

It's also important to state that some features were replaced inline with alternative code, such as const and let moving to var. Others required extra code, such as promises. All supportive code was added to your bundle regardless of your actual use of the features, possibly adding dead code and raising the bundle size for no good reason.

One last thing I'll mention is that the yearly presets did not allow configuring the browsers one would want to support, so I had to adapt the polyfills accordingly.

Major changes with preset-env

The main idea behind preset-env was to stop creating yearly presets and just have one library which will update periodically. But this change, together with the changes in the babel library itself, led to the following shifts:

Moving from babel-polyfill lib to core-js

To provide modules for supporting new JS features, Babel moved away from their own polyfill library in favor of core-js. When I first ran the new configuration on my code, I was surprised to see code addressing browser issues with apis like array.reduce, setTimeout and array.indexOf. As the documentation for each module in core-js is pretty much non-existent, I cloned the repo and looked into the code and commit history for hints on what these modules actually do. I figured out shortly that core-js not only makes ES6→ES5 transformations (described as plugins), but also adds modules for fixing unsupported syntax or partial implementations for older JavaScript features. It also adds support for new features like symbols within old features, described as polyfills. Each module has code it runs within the browser to determine if it should run, replacing some functionality, according to browser type and version.

The preset-env useBuiltins flag determines if babel should always add corejs polyfills, add them once per file or per usage of a particular feature in your code. This is the recommended way: adding only what supports your code. Using the preset-env debug:true flag, I noticed babel pointed out which file of mine required each module within core-js while using useBuiltins: usage. Reading into the core-js library, I learned that its logic of identifying whether you actually use a feature in your code or not is flaky, sometimes relying on appearances of certain keywords and in a few cases adding unnecessary code.

Rollup test

Read this if you are a JavaScript developer: It's important to state that core-js modules, or polyfills, impact browser prototypes in many ways. As we maintain a third party script, we want to avoid changing browser JavaScript implementations on our customers’ websites which can cause unpredictable behavior and bugs. With that in mind, we actually ended up using useBuiltins: false to disable all polyfills, leaving only inline transformations. In our case, core-js added about 90 modules by default, impacting bundle size significantly. I recommend checking what it's adding and including or excluding accordingly. See the exclude\include properties. At HUMAN, we add any plugins we need inline, scoped to our third party script, not affecting browser JS implementations.

Browser Targets

Preset-env also introduced the targets feature, allowing us to tell Babel which browsers we intend to support. This capability is based on the browserslist package for defining our targets. Targets will determine which features we need to transpile or add supporting code for, as more code means a bigger bundle. The Browserslist spec lets us define our targets in a few ways. Here are some possible query parts:

  • Set minimum supported version for browser such as Firefox > 20 or specific version such as ie 6-8
  • Last X versions of each or specific browser last 2 versions
  • According to the worldwide usage percentage for browsers > 5%

Because it was important to me to keep the bundle size small, I compared different percentages and learned there was a major difference in bundle size when adding support for old browsers like IE10. However, once you support old browsers with minor ES6 support, the bundle size won’t change significantly if you decide to support very old ones like IE9 or browsers with a minimum of 0.01% usage as I did. By installing the npm browserslist package you can run the browserslist cmd in the project root to get the minimal browsers support for different browser types.

Browsers list

Your Two options

In my eyes, there are two ways to go with your configuration:

The Safe way - set up useBuiltIns to false, disabling any added code or plugins, leaving only inline ES6 to ES5 transpiling. This way you make sure your code is transpiled to support older browsers but don’t take advantage of other browser bugfixes corejs has to offer. I recommend this config for third party script developers. Here is an example:

Babel helpers

The supporting way - set up useBuiltIns to usage, adding plugins upon usage and including or excluding plugins according to your needs. This way you can support old browser bugs as well as transpile your code. You will also control the effect the corejs has over your bundle size. I recommend this configuration for Full stack developers working on their own web-app.

Babel Helper preset env

Conclusion

Whether you are a full stack developer or javascript developer, it's important to be aware of your babel and preset-env configuration as it can affect behavior as well as bundle size and browser compatibility. Good luck!