Polyfilling a web app the right way
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 ⭐️!