Differential serving vs. polyfill service: How to best serve modern and legacy browsers
Most people today use modern browsers, yet we still have a small but important percentage of traffic from legacy browsers like Internet Explorer.
There are two things modern browsers can do but legacy browsers cannot. First, modern browsers support many new syntax offered in ES2015+, such as ES module import
, arrow function, object destructuring, etc. For legacy browsers, these new syntax must be transpiled to compatible forms, usually with Babel.
Second, modern browsers support many newer ES2015+ functions, such as Promise, fetch, Array.proptype.includes
. For these functions to work in legacy browsers, the corresponding polyfills must be provided.
To ensure our app runs on every device, we often transpile all new syntax to ES5, and provide all polyfills needed for legacy browsers. Yet transpiling and polyfilling will increase bundle size and slow down app. Result is, to be compatible with a small fraction of browsers, the majority of the users have to run an unnecessarily bloated app 🤷♂.
There are two popular solutions to better serve modern browsers while keeping legacy browsers happy: polyfill service, and the so-called “differential serving”. This post talks about what they are, and their pros and cons.
Polyfill service
Polyfill service is…a service that provides polyfill 😂. You can send a request to the service and get back a js script containing all polyfills your browser needs. The service dynamically decides what polyfill is needed by checking the user agent in the http request, which contains information about the client browser. For modern browser like Chrome, the returned polyfill file can be less than 1k, while response for IE can be larger.
Polyfill.io is a popular polyfill service provider maintained by Financial Times. You can also host your own service by using their polyfill module.
Pros of polyfill service
- It is easy to implement. Simply add a script tag in your html and you don’t need to worry about polyfill any more
2. It can be cached long term. Since polyfill seldom changes, user does not need to download them again and again. The fact that the polyfill is decoupled from app code makes long term caching possible, even whenyour app code changes. A potential issue then is how to invalid the cache when the polyfill service itself changes and you do want user to re-fetch a new version of polyfill. But that can be solved with api versioning.
3. It is sharable across apps. Imagine you have large website that consists of multiple web apps. Each web app can share the same polyfill instead of asking the user to download polyfill repeatedly.
4. It has fine grained control of what polyfill is needed for each browser. Since polyfill is dynamically determined by request user agent, it is possible to provide a minimum required size of polyfills needed for each browser.
Cons of polyfill service
- It does not offer solution for ES2015+ syntax. You probably still have to transpile code into ES5, which you miss some performance gains and size reduction for modern browser.
- It adds an extra blocking http request. The browser makes another request to fetch polyfill, and since other app code may depend on the polyfill, app code is blocked from execution before polyfill gets returned. In our measure, the turnaround time can take up to 3s in Chrome simulated slow 3G, which is a bit concerning considering that it is a blocking request.
- Polyfill implementation may not be 100% correct. People had issues due to the way polyfill is implemented in the service. The polyfill module is maintained by Financial Times. Although it is open sourced, it might not have a community as wide as Babel or
core-js
(polyfill Babel uses) does (polyfill-library
gets ~60 stars whilecore-js
has ~8k stars on GitHub). - If you are hosting polyfill service yourself rather than using
polyfill.io
for whatever reason, maintaining the service is another complexity add-on.
Differential serving
The differential serving approach provides two different bundles for modern browsers and legacy browsers. “Modern browsers” refer to those that support ES modules. They are able to execute scripts with type equal to module
: <script type="module">
. Because these browsers often support many ES2015+ syntax and functions, we are able to do less transpiling and ship less polyfill.
Pros of differential serving
- It optimizes both transpiling and polyfilling for modern browsers.
- It can provide minimum polyfills based on your usage. While it does not decide polyfill for each browser like polyfill service, it can provide only those polyfills that are really needed because it can do a static analysis of your code. This feature provided by babel is still experimental, but is really promising.
- Little maintenance is needed. While the first setup with babel and webpack can be confusing, and it does make the config more complicated, it is a once and for all deal. You rarely need to change it, and webpack and babel will always generate the right bundles for you.
Cons of differential serving
- Long time cache for polyfills is more difficult because the cache is bundled with app code.
- It adds complexity to tooling configuration. Configurations of webpack and babel is perhaps already confusing, and differential serving does introduce more complexity. But again once you set it up, it requires little effort to maintain it. And the set up is actually not that confusing once you understand it.
- It may increase build time since we are generating an additional set of bundles.
Implementation of differential serving
The key is in babel. Babel uses target
to decide the browser it will conform to. You can explicitly set a list of browsers, or use esmodules
to refer to modern browsers that support ES module.
Babel supports multiple environment. You can set different configs under different environment name. The babel config looks like:
The option useBuiltIns
is confusing but important. It has three values: false
, entry
, and usage
. false
means you don’t need polyfill. Say if you are using polyfill service and you don’t need babel to provide any polyfill, you can set it to false
.
If you set it entry
, you will need to add import @babel/polyfill
in the very beginning of your app entry code. But don’t worry, you will not include the whole polyfill. Instead, it replaces @babel/polyfill
with polyfill the targeted browsers need.
The value usage
is most powerful, but is experimental at the moment. It will analyze your code and only import polyfill you code needs. You don’t need import @babel/polyfill
anywhere in your code. Instead, babel will import the corresponding polyfills on top of each of your file. Also, you don’t need to worry about including the same polyfill repeatedly in your bundle, because each import only adds a reference to the content in core-js
, and the real polyfill content will only be bundled once.
In webpack config, you need to use babel-loader
to load the babel config. You can pass the babel environment name using option.envName
.
Another critical aspect is webpack’s multi-compiler mode. To generate two bundles, webpack needs to pick two configs, with one specifying the “modern” babel environment name, and the other specifying “legacy” environment name. To do that, instead of exporting a configuration object in webpack.config.js
, export an array of configs:
You are good to go! You will see two bundles generated in .build/js
: one ending with esm.js
and the other with .js
.
Summary
Below is a summary table comparing polyfill service and differential serving. Hope this post helps you decide which approach to use based on your situation.
If you find this post useful, don’t forget to give me your 👏 (s) and follow me on twitter https://twitter.com/imDongCHEN