Conditionally load polyfills

Published: February 14, 2018  •  javascript

In my previous blog posts about Parcel and Polyfill.io I showed you ways how you can add polyfills to JavaScript and TypeScript projects.

A common solution is adding the polyfills to the main application bundle. Another solution is loading polyfills from a service like Polyfill.io.

Adding polyfill code to the main application bundle has the disadvantage that it increases the size of the application and every user has to download it even if they use a modern browser. A service like Polyfill.io solves this problem quite elegantly and only sends polyfills to the browser that they need, but it has the drawback that the browser needs to make one additional blocking http request.

A better approach would be to download polyfills only if the browser needs them. In this blog post I show you two approaches how you can achieve that.


Loading polyfills with Parcel's code splitting functionality

In the first example I show you how you can leverage Parcel's built in support for code splitting and use it for loading polyfills.

The application is a demo for my SSE EventBus library. It requires EventSource support for server-sent events and Symbol for the for of loops. The code uses ES2015 constructs so I run it through Babel to get ES5 code that also runs on older browsers like IE11.

First I added the needed polyfills to my project.

npm install event-source-polyfill
npm install core-js

Next I created a JavaScript file that imports all the required polyfills.

import "event-source-polyfill";
import "core-js/modules/es6.symbol";

polyfill.js

In the main entry point (main.js) the application first checks if the browser supports the required features. If every feature is built in, the code starts the application without loading the polyfills.

import App from './app';
import "core-js/modules/es6.promise";
import "core-js/modules/es6.array.iterator";

if (browserSupportsAllFeatures()) {
    new App().start();
} else {
    import('./polyfill').then(() => new App().start()).catch(e=>console.log(e));
}

function browserSupportsAllFeatures() {
    return window.EventSource;
}

main.js

In case not all features are supported the application loads the polyfill.js with a dynamic import(). During build time the dynamic import signals Parcel to not include the polyfill.js into the main application bundle and instead create a separate bundle. Parcel also automatically injects code that loads the module if needed during runtime.

Unfortunately this feature by itself depends on two features (Promise and iterator) that are not available on a ES5 browser like IE11. That's the reason for the two import statements on line 2 and 3.

After a build with npm run build, you find two JavaScript files in the dist folder. The smaller one contains the polyfills imported from the polyfill.js file.
When you load the application in a modern browser, you see that only the main bundle is loaded. In IE11 both JavaScript bundles get loaded, first the bundle with the polyfills and after that the main bundle.

You find the complete source code for this example on GitHub:
https://github.com/ralscha/sse-eventbus-demo/tree/master/client


Loading polyfills with script tag injection

For the second approach I use code from Philip Walton's blog post about loading polyfills and combine it with Polyfill.io.

Instead of always sending the request to Polyfill.io the following example first checks if the browser supports all the necessary features, if not the applications fetches the polyfills with a GET request from Polyfill.io.

The Polyfill.io script is loaded dynamically with the loadScript method. This is a method I copied from Philip Walton's blog post. The method expects two parameters, the URL to the JavaScript resource and a callback which is called as soon as the browser loaded the resource.

import App from './app';

if (browserSupportsAllFeatures()) {
    new App().start();
} else {
    loadScript('https://cdn.polyfill.io/v2/polyfill.min.js?features=EventSource,Symbol,Array.prototype.@@iterator,Array.prototype.entries', function () {
        new App().start();
    });
}

function browserSupportsAllFeatures() {
    return window.EventSource && window.Symbol && Array.prototype.entries;
}

function loadScript(src, done) {
    var js = document.createElement('script');
    js.src = src;
    js.onload = function () {
        done();
    };
    js.onerror = function () {
        done(new Error('Failed to load script ' + src));
    };
    document.head.appendChild(js);
}

main.js

loadScript dynamically adds a script tag to the DOM with the src attribute pointing to the given URL. When the browser has loaded the resource, it emits the load event and the load handler calls the provided callback function (done).

An advantage of this approach is that the code in main.js does not depend on any polyfills and should run fine in older browsers, because of this the application bundle is about 24 KB smaller than with the first approach. This is the size of the two polyfills I had to add for Parcel's code splitting support.
If you don't like the dependency on a 3rd pary service, it is quite easy to install Polyfill.io on your own server. See my blog post about this topic.

You find the complete source code for this example here:
https://github.com/ralscha/sse-eventbus-demo/tree/master/client2