JavaScript Bundle Optimization – Polyfills

If you are lucky enough to only support a small subset of browsers (for example, you are targeting a controlled set of users), feel free to move along. However, if your website is open to the broader internet, you are likely dealing with polyfills in an attempt to support as many different browsers and devices as you can afford. While polyfills can help in making our websites more accessible, the polyfills included and, more importantly, how you include them can have a measurable effect on first contentful paint and user experience.

What is a Polyfill?

A polyfill is a piece of code (usually JavaScript on the Web) used to provide modern functionality on older browsers that do not natively support it.

via Mozilla Developer Network Web Docs

In a perfect world, every one of your users would be using a browser that supports every one of your favorite JavaScript, CSS, and browser features. Whether it be CSS Grid or JavaScript promises, everything would work as expected. But, as this isn’t the case, polyfills allow for us to use the latest browser-features in our code by including the code to fill in missing features.

Unfortunately, including polyfills with your deployed code brings with it two problems:

  1. You need to know and keep track of what polyfills to include within your code bundles.
  2. If you bundle your polyfills with your core code, they are downloaded by all users – not just those that need them.

While polyfill-heavy browsers like Internet Explorer 11 are gradually becoming more officially obsolete (So You’re Looking to Drop IE11 Support), it is still worth spending some time thinking through what browsers you support and the appropriate balance between performance and developer experience for your project.

So, how do we support older browsers without bogging developers down with more maintenance and without penalizing the bulk of our users? The following is a collection of three* approaches that might be useful (individually or in combination) depending on the requirements of your specific application.

1. Babel preset-env (and browserlist)

the Babel tip

Chances are if you are building a “modern” web application you are likely already using Babel in some capacity. To improve developer experience and ensure only necessary polyfills are shipped to the end-user, Babel’s @bable/preset-env is a great place to start.

At a high level, @babel/preset-env dynamically transforms JavaScript syntax and injects needed core-js polyfills as needed by the application to support a specific set of browsers (you can read more about browserlist configs here). Specifically relating to polyfills, we are interested in the useBuiltIns config option and the following two settings:

  1. entry – all polyfills are injected at a single location. Useful in low-polyfill-count applications where all polyfills upfront does not negatively impact first contentful paint.
  2. usage – polyfills are injected in the file in which they are used, leaving it up to the bundler to only load them once. Useful for applications that see performance benefits of spreading polyfill cost across various bundles.

Which setting you use is entirely dependent on the unique requirements and performance needs of your application. Do keep in mind that only core-js polyfills are automatically injected and that you will still need to be aware of any other feature gaps you may have.

2. Lazy-load

the Webpack tip

Whether you use @babel/preset-env or not, you will likely have some polyfills that need to be manually maintained. Although this means we still have a developer burden, it does provide an opportunity to be selective about when we load application’s polyfills.

By using a combination of feature detection and lazy loading, we can reduce the polyfills that modern browsers need to download, while still fully supporting old browsers. At its simplest, it may look like the following:

// Check if the browser has a fetch implementation - if not, download & polyfill
if (!window.fetch) {
  await import('isomorphic-fetch');
}

This approach works fairly well for JavaScript features, but you might have to get more creative when it comes to deciding whether CSS or browser features need polyfilling.

if (isSomehowLegacy) {
  await import('intersection-observer');
}

One last thing to note on this approach—the examples above may result in individual file downloads for each polyfill. Having a bunch of individual polyfill file downloads on your initial page load may be a performance hit, so some tweaking of your webpack bundle chunking may be necessary.

3. Multiple Bundles

time to over-engineer

Another approach to reducing the amount of code served to users is to provide different code bundles to different browsers. This allows you to reduce polyfill bloat when serving users using newer browsers, leading to faster download times.

I know I said I would be talking through three approaches, but this is where I cheat and list three more similar, but different approaches.

3.1 Dual Builds – ES6 Modules

If you are working with a serverless client-rendered application, one multi-build option to consider is to generate two sets of JavaScript bundles – one set of bundles for browsers that support ES6 Modules and one for those that do not.

<!-- Browsers supporting modules download "main.mjs" -->
<script type="module" src="main.mjs"></script>

<!-- All other browser fall back to the more polyfill heavy "main.bundle.js" -->
<script nomodule src="main.bundle.js"></script>

I won’t go into much detail on the specifics of the approach here, as these two articles already outline the approach in detail quite well:

I will, however, call out that your browser support requirements may make this technique unnecessary. Can I Use estimates that 92%+ of global traffic already supports ES Modules, with most desktop browsers having supported ES6 modules since 2018. Unless you need to support Internet Explorer or certain mobile browsers, the differences between your bundles will likely be minimal. It is worth taking a look at Can I Use before implementing to see if this approach will have a worthwhile impact with your specific needs.

Vue

Are you using Vue with the Vue CLI? You’re in luck! The CLI has support built-in for generating “modern” module bundles by using the —modern flag. See the CLI documentation for more info.

3.2 Custom Polyfill Bundles – Polyfill.io

If you are willing to use a third-party service for polyfills or can run your own standalone polyfill sever, Polyfill.io is another option for providing different polyfill bundles to different users.

a service which accepts a request for a set of browser features and returns only the polyfills that are needed by the requesting browser.

Polyfill.io uses server-side User-Agent sniffing to generate a polyfill bundle per user. This means that as a developer, you no longer need to bundle in polyfills supported by the service and can instead leave it up to the service to provide each user with exactly what they need. The service can be used directly or you can manually host your own server using their open source node service polyfill-library.

In use, your application can get any supported polyfill that the user might need:

<script
  src="https://polyfill.io/v3/polyfill.min.js"
  type="text/javascript"
></script>

Or, only request specific polyfill your application might need:

<!-- Only retrieve es5, es2015, Array.isArray -->
<script
  src="https://polyfill.io/v3/polyfill.min.js?features=es5%2Ces2015%2CArray.isArray"
  type="text/javascript"
></script>

There are a few things to consider when looking at this approach. First and foremost, as this is an all-or-nothing approach to polyfills, your application’s initial paint may be delayed until the bundle has been downloaded. Deferring polyfill downloads that are only needed by the application later is a manual process. Also worth keeping in mind, if you want to minimize the number of polyfills that the user has to download, the developer will once again need to manually define the needed polyfills.

3.3 n Bundles + User-Agent Sniffing

This poorly named section combines most of the approaches that have been talked about so far into a hybrid that provides a balance between the developer experience and user experience.

At a high level:

  • Setup your application to take advantage of the automatic polyfilling support of @babel/preset-env
  • Lazy load any additional polyfills as needed.
  • Determine how many different builds you might need (the sky is the limit, but consider limiting to a two-build modern & legacy setup) and configure browserlist to reflect the different support browsers.
  • Finally, use User-Agent sniffing server-side to appropriately server up code to users (based on your previously defined browserlist config).
    • As a bonus, in addition to “modern” bundles having fewer polyfills, the need to transpile JavaScript code down to ES5 is also removed, potentially leading to faster parsing times by newer browsers.

For a fully detailed example, see Smashing Magazine’s write-up on how to tie your browserlist config file into a modern/legacy dual-build setup.

With a solid combination of simplified developer experience and improved user experience – what’s not to like? While day-to-day developer experience may improve, by serving multiple potentially unique code sets to different users, testing and debugging of issues may become more complex. Similarly, this approach also clearly increases the complexity of an application’s build configuration.

Wrapping Up

So, what’s the best approach? In my personal opinion, stating with @babel/preset-env for core-js polyfills while lazy loading anything else tends to gets things moving in a good direction. But as always, it depends on your specific requirements and application.

About the Author

Daniel Testa profile.

Daniel Testa

Principal Consultant

A frontend developer with a focus on Javascript technologies.

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Blog Posts
Feature Flags in Terraform
Feature flagging any code can be useful to developers but many don’t know how to or even that you can do it in Terraform. Some benefits of Feature Flagging your code You can enable different […]
Infrastructure as Code – The Wrong Way
You are probably familiar with the term “infrastructure as code”. It’s a great concept, and it’s gaining steam in the industry. Unfortunately, just as we had a lot to learn about how to write clean […]
Snowflake CI/CD using Jenkins and Schemachange
CI/CD and Management of Data Warehouses can be a serious challenge. In this blog you will learn how to setup CI/CD for Snowflake using Schemachange, Github, and Jenkins. For access to the code check out […]
How to get your pull requests approved more quickly
TL;DR The fewer reviews necessary, the quicker your PR gets approved. Code reviews serve an essential function on any software codebase. Done right, they help ensure correctness, reliability, and maintainability of code. On many teams, […]