The disadvantages of Javascript polyfills

A polyfill, also known as a shim, is a user-defined implementation of an API that some browsers provide natively, normalising browser differences.

As a proponent of outside-in development, I see the lure of trying to develop as if all browsers are the same. The problem is that browsers are not the same. Polyfills are problematic because:

1. They augment host objects

Polyfills augment host and native objects. Experts such as Richard Cornford, David Mark, Thomas Lahn and Kangax have told us this is a bad idea. The latter of which published two articles on the subject:

Here’s a choice snippet:

“DOM extension seemed so temptingly useful […]. But what hides behind this seemingly innocuous practice is a huge load of trouble. […] the downsides of this approach far outweigh any benefits.”

2. The full API is rarely needed

We may not need the full API to solve the problem. We may not even be able to implement a polyfill because there’s just no way to do so. This is why context is important.

We should first look to understand the problem precisely. And then solve that problem only. We rarely need all of an API, which I’ll demonstrate in a moment. With polyfills it’s all or nothing.

3. They come with caveats

It takes little effort to find problematic polyfills. Take the ES5 Shim documentation. In describing the Object.create polyfill it states:

“For the case of simply “begetting” an object that inherits prototypically from another, this should work fine across legacy engines.”

The word should doesn’t instill confidence. We should, of course, build atop of reliable foundations—we are only as good as our lowest level functions. It continues:

“The second argument is passed to Object.defineProperties which will probably fail either silently or with extreme prejudice.”

We shouldn’t expect our team to rely on code like this, much less our users. Any code we write around this is just ‘polishing a turd’.

What should we do instead?

A facade or wrapper, is a design pattern that simplifies an interface around something more complex. This lets us abstract away the differing browser implementations and bugs. And with the added bonus of being able to simplify the method signature.

Inside the wrapper there’s nothing to stop us using bits of an API, and feature testing various implementations and acting accordingly, like Peter Michaux shows us in Cross browser Widgets.

Cloning an object is pertinent to this article because Object.create solves this problem. If we want to support modern browsers only—that is those that provide Object.create—then an implementation might look like this:

var lib = {};
if(Object.create) {
  lib.cloneObject = function(obj) {
    return Object.create(obj);
  };
}

As this implementation only uses a small part of the entire API, the exposed method signature has just one argument, solving the precise problem and no more. But what about browsers lacking Object.create?

If we want to degrade gracefully, we don’t have to do anything (hello Progressive enhancement!). If we want to support other browsers add a second fork:

// Code credited to David Mark. Thanks.
var lib = {};
if(Object.create) {
  lib.cloneObject = function(obj) {
    return Object.create(obj);
  };
} else {
  lib.cloneObject = (function() {
    var Fn = function() {};
    return function(obj) {
    Fn.prototype = obj;
      return new Fn();
    };
  })();
}

The problem got a bit more challenging but the implementation is still lean and the method signature holds up. But, we didn’t need to worry about recreating Object.create in its entirety.

But if we did need the full API, we could make two changes. First, change the name of the function to something more appropriate. Second, expand the method signature to allow for property descriptors like this:

var lib = {};
if(Object.create) {
  lib.createObject = function(obj, props) {
    return Object.create(obj, props);
  };
}

What about browsers lacking Object.create? Same as before. Either degrade gracefully or add another fork. This is progressive enhancement.

Summary

At best, polyfills are hard to implement and complicate matters by intertwining browser and application logic. This complexity is costly.

At worst, polyfills have caveats and gaps that cause pain for the developer and broken experiences for users.

Instead, use wrappers, which enable us to build reliable, progressively enhanced and therefore accessible experiences.