Polyfilling a JavaScript library the right way

Lee Cheneler
3 min readApr 23, 2019

--

I recently wrote a post about polyfilling a web app (which you can read here) and it got me thinking about whether you should always polyfill JavaScript libraries you write.

Why would we polyfill a library?

People build applications that need to run in all sorts of environments, from older browsers to servers running older versions of node. Unfortunately only supporting newer browsers or always keeping your node installations up to date isn’t an option for everyone.

If we want our library to be consumable by as many people as possible then we need to ensure it runs on as many platforms as possible. In many environments modern JavaScript features such as Set or Promise are not available. In order to leverage these then we need to polyfill those features into our libraries bundle.

Beware traditional polyfilling

You need to be careful that you don’t use a traditional polyfilling approach like you usually would when building a web app. Traditional polyfilling involves you adding features globally so they’re available on the global namespace. We don’t want our libraries to do this as it is not our place to change the users environment unbeknownst to them. Libraries should bring functionality without producing side effects.

Take Promise as an example. Fairly recently Promise got a new feature, .finally(). We might bundle a global polyfill for Promise in our library that is slightly out of date and doesn’t provide an implementation for this newer feature. The user might apply a global Promise polyfill themselves that supports .finally() and our library might overwrite it, thus breaking the user’s code wherever they use .finally(). This is why it is very important that our libraries do not pollute the global namespace.

An automated approach

Babel provides a nifty solution to automatically provide polyfills with something called @babel/runtime along with a plugin called @babel/plugin-transform-runtime. It works in a similar way to @babel/polyfill but does not pollute the global namespace.

What @babel/plugin-transform-runtime does is detect when we use features that will need polyfilling such as Set or Promise and will import the relevant polyfill as a module from @babel/runtime so it is scoped to the file it is required in. This means it does not pollute the global namespace and if you are using a module bundler such as Webpack then it will ensure that the polyfill is only included in the bundle once saving on bundle size.

This is a great approach that allows us to use modern language features without needing to manually manage polyfills or worry about altering the environment outside of our module.

To configure this in Babel we first need to install the dependencies.

yarn add @babel/runtime
yarn add --dev @babel/plugin-transform-runtime

Then we need to configure the plugin in our babel configuration file:

// babel.config.js
{
plugins: ["@babel/plugin-transform-runtime"]
}

Now we can write code that utilises Promises, compile it with Babel, then run it in a version of Node that does not support Promises and it will work.🤘

There are some exceptions such as instance methods (foo.includes(“bar")) that will not be provided by this transform. They would require altering global instances which is a side effect we don’t want.

Alternative approaches

Some libraries don’t take the above approach and thats absolutely fine!

Your library may be so small that it’s simply less work to not use a modern feature that will need polyfilling.

Another approach some libraries take is to use modern features that will need polyfilling, but not provide the polyfill themselves. They document what polyfills you need to provide if you want it to run in older environments. React is a good example of this. It uses Sets internally and so if you want to run your React app on a browser that doesn’t support Sets then you need to provide the polyfill yourself.

These approaches can help further reduce bundle size as if the consumer of your library is already using Set for instance then you including a polyfill locally in your library is unnecessary.

In conclusion

If you want write your library using modern JavaScript language features and have it be easily consumable then you’ll want to bundle in your polyfills. Using @babel/runtime makes this super simple, helps reduce bundle size by using modules and doesn’t pollute the global namespace. Thanks Babel you ⭐️!

--

--