Home | Send Feedback

Conditionally load polyfills

Published: 14. February 2018  •  javascript

A common solution is adding the polyfills to the main application bundle. Another solution is to load 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 need them, 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 examples of 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.

Parcel automatically takes care of transpiling the JavaScript that uses ES2015 features to ES5 compatible code.

To solve the missing function on older browsers, I added these two polyfills.

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

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

import { EventSourcePolyfill } from 'event-source-polyfill';
import "core-js/modules/es.symbol";

window.EventSource = EventSourcePolyfill;

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/es.promise";
import "core-js/modules/es.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

But if the feature is missing, the application first load polyfill.js with a dynamic import(). During build time, the dynamic import signals Parcel to not include the code from 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 Parcel feature by itself depends on two features (Promise and iterator) that are not available on an ES5 browser like IE11. That's the reason for the two import statements on lines 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 application 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 is finished loading the resource.

import App from './app';

if (browserSupportsAllFeatures()) {
    new App().start();
} else {
    loadScript('https://cdn.polyfill.io/v3/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 bundled polyfills and still runs fine on 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 party 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