Polyfills: everything you ever wanted to know, or maybe a bit less
The browser world we live in today is pretty great. You can use promises and fetch and arrow functions and const and let — all the hip new gear — and it will work in every major browser. No polyfills, no transpiling, it will Just Work.
That’s frickin amazing!
But it’s not frickin relevant.
Because some of your website visitors use ye olde internet explorer, or UC browser, or Safari from two weeks ago. And you want to take their money just a bad as everyone else’s.
Faced with the reality that you can’t write modern code and expect it to work for all users, you have exactly two choices:
- Only use language features available in all the browsers you support
- Write modern code, then do something to make it work in older browsers
If you have decided on option one then I respectfully suggest that you are bonkers, and insist that you state your case in the comments.
I believe that developers who are able to explore all the new stuff and use it in their day-to-day jobs stay happy, and being happy is important.
And so, let us set about making our modern code run in non-modern browsers. We can achieve this in one of two ways…
(If you’re Captain Advanced, you may want to skip the primer and head on down to Polyfills and performance.)
Transpiling vs polyfilling
For quite a while I wasn’t clear on the difference between transpiling and polyfilling, as strange as that seems to me now. So on the off chance that you’ve never had the talk about transpiling and polyfilling, let me attempt to explain.
A transpiler takes the syntax that older browsers won’t understand (e.g. classes, ‘const’, arrow functions), and turns them into syntax they will understand (functions, ‘var’, functions).
So you can write devilishly handsome code like this:
Then run it through a transpiler such as Babel, and it will be turned into this glorious monstrosity:
The syntax, you will note, has been changed into something that all browsers will understand.
You might be thinking “hey neato, this code will work in IE9 now because it’s been transpiled”!
I applaud your attitude, but you are wrong.
For you see, I used the new
includes array instance method. And if you look carefully at the transpiled code, it’s still there. You would be forgiven for thinking that
arr.includes(item) would be transpiled into
arr.indexOf(item) > -1 or something.
Forgiven, but still wrong.
So our syntax has been sorted out, but we still have code that uses ES2016 features, and these will not work in IE9 — because that year didn’t exist when IE9 came out.
So, transpiling gets us some of the way there, but we need a way to teach these older browsers what ‘includes’ means.
[psst, polyfills heading, that’s your cue]
A polyfill is code that defines a new object or method in browsers that don’t support that object or method. You can have polyfills for many different features. Maybe one for
Array.prototype.includes, and two more for
If you want to check out what a polyfill actually looks like as readable code, MDN often has examples of polyfills for particular features, and does not let us down when it comes to array.includes().
Transpilers and polyfills can do everything, that’s wonderful!
I applaud your efforts to use the word ‘awesome’ less, but no, you’re wrong.
Transpilers and polyfills can’t do everything.
It isn’t always clear what can be polyfilled or transpiled, especially if you don’t know much about how the feature in question works. Here’s a spattering of features to give you an idea of how each can be converted to be used in older browsers.
A good general rule is this:
- If it’s new syntax, you can probably transpile it
- If it’s a new object or method, you can probably polyfill it
- If it’s something clever that the browser does outside of your code, you’re probably SOL
But I’m afraid that my peach table belies some gray areas.
window.crypto be polyfilled? Hmmm, my legal team doesn’t want me to answer that.
Element.scrollIntoView() be polyfilled? Well, pro tip: if a polyfill requires jQuery to work, I suggest you run — not walk — in the opposite direction. And not that pointless run people do when crossing the road. I mean chased-by-octo-dog run.
OK are you back from all of those hyperlinks? Good.
Where can I find polyfills?
There is a central registry of all polyfills that have been vetted by the W3C called polyfill-central.
Of course there is no central anything, this is web development; there is the usual mess of sources and varying levels of quality.
Babel to the rescue again. A lot of what you will ever need you will find in
babel-polyfill or more specifically:
core-js (which is what babel-polyfill uses). If the feature you want to polyfill is in there, use it.
(Quite a few people seem to have their favourite Promise polyfills, which I don’t understand. But I also don’t understand parties or the recent creative decisions of Lilly Allen, so whatever. My suggestion: use the promise polyfill in core-js.)
If the polyfill you want is not in core-js, try:
- caniuse.com (the resources tab sometimes has polyfills)
- check if MDN has something you can copy/paste
- if still no luck, then just search on AltaVista (or whatever hipster search engine you use)
Here seems a good place to throw in a usual caution: people that write polyfills do not require a licence.
I’d happily use
core-js without batting either of my eyelids. But I wouldn’t touch the above-linked scrollIntoView() polyfill with a pole of any dimensions. And I would test this classList polyfill as well as I’d test my own code, simply because it’s from an unknown (although 800+ stars and an inbound link from caniuse.com is a good sign).
Okey doke, artichoke, that’s a brief-enough overview of polyfills, but before you go and include a polyfill for everything, you should understand a bit about …
Polyfills and performance
Story time: I recently wanted to format a number as a price, turning
$1,234.57 (there were other numbers I wanted to format as prices too).
It’s not pretty but it gets the job done. A kindred spirit.
Of course, there are downsides. Although support is pretty solid, it’s not 100%. So if you’re supporting SafarIE9, you’ll want a polyfill.
Specifically, you’ll want to polyfill the Internationalization API which defines the parameters that go into
(Golly gosh internationalization is a long word. If only there was some way to compress those 18 letters between the i and the n.)
Intl polyfill with a handful of locales is — get this — 214 KB (for shock value), or 25 KB (minified and gzipped).
To polyfill all the way up to ES2017 is another 27 KB. There’s still fetch and plenty of others, too.
We can’t just stuff all of these polyfills into our app, the other kids will call it fatty boomsticks.
Our goal is clear, we need to …
Load less code
First, we must understand three quite different methods for shipping less polyfill code to a user’s browser:
- Polyfill fewer things by targeting only newer browsers
- Only send polyfills to browsers that need them
- Only polyfill things used in your code
Let’s look at each in excruciating detail.
Polyfill fewer things by only targeting newer browsers
Babel has a tool that will reduce the amount of features that are polyfilled based on the browsers you are targeting.
For example, if the oldest browser you support is IE11, you don’t need to polyfill
Array.prototype.forEach(), because that’s been around since IE9.
This is not a good idea.
core-js package, bringing the size of your app up to 127 KB.
You become stranded in Madagascar and a talking lemur teaches you the importance of self-control, and you decide that you probably don’t need to polyfill everything.
You (when you’re back from Madagascar) use
babel-preset-env and set
true, and target only as far back as IE11.
When babel transpiles your code, it will replace any references to
core-js with references to all the individual polyfills required to satisfy the browsers you have specified.
It’s a clever idea and it will reduce the size of your package, so it’s a good thing, right?
Well … your app will shrink from 127 KB to 121 KB.
A measly six kilobytes.
We could discuss whether or not this is worth it all day long (it isn’t). But there’s something else wrong with this approach.
You’re shaving 6 KB off, locking out a bunch of users with really old browsers, and then shipping that code to all of your users with current browsers, even though they don’t need the polyfills at all.
So how about this: instead of shrinking the polyfill bundle by a few percent and losing customers, why don’t we shrink it by 0% for the few people on old browsers, and by 100% for everyone else?
To achieve this we will need to…
Only send polyfills to browsers that need them
Before we proceed, I have a question for you. Would you be happy to have a third-party script in your page that blocked the loading of the rest of your page? The third party is reputable and the script is fast.
If your answer is yes, use polyfill.io.
When you include their script in your page, it will make a request to their servers, where they will check out your user agent string and work out what your browser needs to bring you up to the current spec. For example, IE11 will receive 8 KB of polyfills in a rather insanely small amount of time:
(Why is nobody as amazed as I am that a browser can load an asset in 4.62ms? What’s wrong with you people!)
Load the same page in Chrome and you’ll get no polyfills because Chrome is awesome:
Maybe one day I’ll learn to trust again. And on that day I’ll put my precious site performance in the hands of another.
But for now, the scars left by optimizely are still too raw.
Sorry for picking on you optimizely, but c’mon:
A blocking script in the head taking almost 3 seconds? Not at all cool. Maybe have a chat to the polyfill.io guys about Fastly.
Alright, rant complete.
So, like me, you want to hold the performance reins and now you need to write some code to conditionally load polyfills. It will work like this:
- Your script: “hey, browser, do you know how to
- IE11: [grunts and drools]
- Your script: “Don’t fret, my pet — I’ll load a polyfill and teach you how to
But today I’m going to use Webpack and their
require.ensure syntax. If you’ve looked at
require.ensure before and found it confusing, please let me know so that I can feel less dumb.
But, now that I’ve used it, I see that it’s really very simple. Here I will load a fetch polyfill (
whatwg-fetch) if the browser doesn’t know fetch already.
Side note: why do I write
if ('fetch' in window) rather than
if (window.fetch)? Because I don’t trust future David.
You see, if I get into the habit of doing
if (object.prop), at some point he’s going to test if
document.hidden is supported by doing
if (document.hidden) and then the poor idiot will scratch his/my head for days trying to work out why something weird happens only when the site is opened in a new tab.
So, I — like many before me — have got into the habit of testing for the existence of a property rather than the truthiness of a property, even though they’re the same thing 99% of the time.
That snippet above will load one polyfill — fetch. But we don’t want to load just one polyfill. We want to load several of the little blighters.
I’m going to wrap each chunk of polyfill-loading code in a promise, so that I can fire them all off at the same time and then start the app when they’re all done.
Ironically, you can’t conditionally load a promise polyfill with Webpack 2. This is because the conditional loading logic behind
require.ensure now relies on promises. So you’ll be shipping a promise polyfill to the 90% of your users that don’t need it whether you like it or not.
There’s no point crying for me over spilt milk in Argentina though, so suck it up, princess.
Below is the entry point to my app in the browser (client.js) which just loads the polyfills, then continues on mounting the app.
At build-time, Webpack will create your normal bundle, plus three more bundles:
core-js.js (although I can’t get this working in Webpack 2.2, I just get
2.js). When you code runs in the browser, Webpack will fetch these files if it needs to, before executing the rest of your app’s code.
You’ll notice that in the
fillCoreJs function, I just check for a few of the newer features (that I use), and if they aren’t all supported then I load the entire
This all-or-nothing approach might seem unsophisticated, but remember that core-js is only 27 KB, and that it won’t be loaded in Chrome, Edge, Firefox, or Safari 9+.
Although I will make an effort to have my site work in old browsers, I will not waste my time shaving a few KB off for obsolete browsers. Maybe once I’ve finished all of life and there’s nothing left to do I will come back and work on performance for IE9.
To test that your polyfill logic is working, you should be able to delete some existing method/object, and have your site still work. For simplicity I like to do it right at the top of my HTML.
So that’s all there is to say about that. Provided that you don’t stop testing your site in older browsers, you’re at no risk of accidentally forgetting a polyfill.
That third thing I mentioned
Remember I spoke about the three different ways to reduce the size of your polyfills?
Well, I originally started this post because I wanted to create a different way to reduce which features were included in a polyfill bundle. My plan was to do static analysis of all the code in my app (at build time), and then create a tailored polyfill for only the features that I actually used. The idea being that there’s no point polyfilling Symbol and Map and Typed Arrays if I never use them.
Long story short, it’s not worth it. At best, I could remove 11 KB of code that I am only sending to old browsers anyway.
If you’re only sending polyfills to the browsers that need them, then there is zero improvement to be had for the majority of your users.
But the fact that I wasted a bunch of time trying to work out how to do this is really a happy ending.
End of blog post.