Polyfilling a web app the right way

Lee Cheneler
3 min readMar 13, 2019

--

I’ve worked on a lot of web apps over the years and many of them have employed different strategies in order to support their desired target browsers. These days Babel makes this effortless, but it wasn’t always like this.

In the beginning there was nothing

Our app would sometimes break for certain users because they were using a browser that didn’t support a JavaScript feature we were using in our app code. This wasn’t great. Our app could be broken for many users in the wild and the feedback loop was often slow resulting in fixes taking time to be rolled out.

When a problem was reported we’d have to find the JavaScript feature that didn’t work in that browser and then manually provide that JavaScript feature ourselves, often using a library. This is known as applying a polyfill. If we were using a Promise for instance, and a customer was using a browser that didn’t support Promises then we’d have to add a Promise polyfill.

// Define 'Promise' globally
import "es6-promise/auto";

The chore of supporting JavaScript features as they start arriving at pace

With the arrival of ES6 new JavaScript features suddenly started landing thick and fast and it became a real chore to manage these polyfills individually as and when you needed them.

Babel offered a clear solution to this problem, a wide reaching and easy to install polyfill, babel-polyfill (these days released at @babel/polyfill). You simply imported it before everything and it globally applied all the polyfills babel supported, which is basically all of them.

// First line in your applications code
import ‘@babel/polyfill’

Bundle bloat, the age of optimisation

The wide reaching Babel polyfill approach is great, however it has a problem. It includes all polyfills in your app bundle even if you don’t need them. This adds to your bundle size. In an increasingly mobile world we want to reduce bundle size as much as we can. Mobile networks are not as fast or reliable as our home connection. This means that users can have vastly different loading times when on the go.

One solution would be to go back to micromanaging your polyfills, but this isn’t attractive, it is easy to miss one and accidentally release code that doesn’t work in a particular browser.

@babel/preset-env to rescue

Luckily for us we don’t need to go back to micromanaging out polyfills. Babel can do that for us these days with @babel/preset-env.

It works in conjunction with Browserslist to only apply transforms and polyfills when it needs to. For instance, it will only include the Promise polyfill if you have used Promises and your target browsers don’t already support them.

Setting up @babel/preset-env

Set up should not take long. We need to install the the @babel/preset-env and @babel/polyfill. Do not import @babel/polyfill yourself in your application this time though.

# Preset is a dev dependency
yarn add --dev @babel/preset-env
# Portions of @babel/polyfill is bundled into
# your app so its a normal dependency
yarn add @babel/polyfill

Then we configure the preset in our babel configuration file. The option useBuiltIns: "usage" tells @babel/preset-env to import only specific polyfills from @babel/polyfill. @babel/polyfill is actually a collection of smaller core-js modules and these are imported individually as required. This takes advantage of the fact that bundlers like Webpack will only include modules in the bundle once.

// babel.config.js
{
presets: [
["@babel/env", { useBuiltIns: "usage" }]
]
}

Bundle savings

Obviously the best result is to not need to ship polyfills. But in todays diverse world of the web its a necessity. The below chart shows just how much smaller you can get the bundle by only shipping the polyfills your app uses.

In conclusion

So now we’ve got the best of both worlds! We no longer need to manage our own polyfills and we’re no longer bundling in polyfills we don’t need keeping our bundle size as small as possible! Thanks Babel you ⭐️!

--

--