Polyfill as needed with polyfill-service
How to maximize the performance of loading polyfills for your application users.
In last week’s newsletter “What is a polyfill”, I talked about a situation I came across with a white screen on IE10 (the app crashed because we were missing polyfills). I explained a bit of the difference between a polyfill and a code transform. I explained a few options you have at your disposal to use new JavaScript features and still support older browsers. In the conclusion I said this:
> So what did I do to fix my IE10 bug? Well, one thing that really bugs me is that I have to ship all this code for polyfills to all browsers even if they do support these features. But a few years ago I heard of a service that was able to ship polyfills that are relevant only to the browser requesting them. I created my own endpoint that uses the module that powers that service and I’ll write about that next week!
That’s today’s newsletter! I’ll explain how I created a polyfill.js
endpoint that gives back a very aggressively cached JavaScript file with the polyfills that users need and no more.
Why polyfill-service?
With the way I have my usage of polyfill-service configured today, if I make a request for polyfill.js
using Internet Explorer 10 (the lowest version of IE that we support), the response is 60.2kb! If you’re unfamiliar with the impact this can make, I suggest you read The Cost Of JavaScript by Addy Osmani (or watch a talk version here). To put this in terms you may appreciate, this will take users in emerging markets about a full second just to download, then you have to take the content they’ve downloaded and parse/compile/run it which can take even longer especially for individuals using lower-end phones.
The state of the art with polyfills is to include those polyfills in your bundle.js
file (in fact, lots of apps are just using all of core-js
which is 84.2 kb of minified JS). This means that every browser will need to download, parse, and run that JavaScript regardless of what browser they’re using. But let’s take a look at browser usage statistics. Your stats may vary depending on your users, but if your app is typical of the global averages, then you have maybe 5% of your users who need more than a handful of kbs worth of polyfills. Most of your users will be using modern, evergreen browsers that support most of the features you’re using. So you’re making users who are running modern browsers pay a “tax” for your site supporting those 5% of users who wont/can’t upgrade.
If I run Chrome 67 on my polyfill.js
file, it comes back basically empty. By using polyfill-service, only the browsers which need the polyfills receive them. This means that they can use my app quicker and I’m not taking up some of your bandwidth to download stuff you don’t need (which actually means saving people actual dollars if they don’t have unlimited data).
Another aspect of using something like polyfill-service is because my polyfills live in a completely different file from my bundle.js
, I can have it cached forever, so users only need to download it once and never need to download it again. So even for users on bad networks, they’ll benefit from not having to expend resources re-downloading a file that will never change.
Using polyfill-service
The polyfill.io service from Financial Times is awesome, but with no SLA (service level agreement), many companies can’t rely on it for mission-critical applications. Luckily, the module that powers it is completely open source so you can set up your own service in-house in a pretty straightforward way and that’s exactly what I did.
With the app I’m working on right now (paypal.me), we have a server that’s responsible for some light server-rendering for SEO purposes. Basically, our server is a NodeJS server using KrakenJS (a wrapper on top of express), so I added a get
handler to the express app:
app.get('/polyfill.js', getBrowserPolyfill)
And with the getBrowserPolyfill
is a typical express route handler:
import polyfill from 'polyfill-service' async function getBrowserPolyfill(req, res) { const script = await polyfill.getPolyfillString({/* options */}) res.set({ 'Content-Type': 'application/javascript;charset=utf-8', 'Content-Length': script.length, }) if (shouldCacheAggressively) { res.setHeader('Cache-Control', 'immutable') } res.write(script) res.end() }
There’s a little bit more to it, but this is the basic idea. So let’s talk about a few aspects of this solution.
User Agent
So the polyfill-service module needs to know what the user agent string is to determine what the script
string should be (which JavaScript polyfills to include). So I pass req.headers['user-agent']
as that value, though I allow the ua
query string to override this and I have a fallback to IE 9 just in case. And in the case that polyfill-service encounters a user agent it doesn’t recognize, I have it configured to just treat it as if it needs all the polyfills (via the unknown: 'polyfill'
option).
Features
There are a LOT of features that polyfill-service supports out of the box. It defaults to the most useful ones, but it’s a good idea to configure it. At first I thought: “Hey, let’s just have it support everything.” But then I found out that if you asked it to polyfill everything it could, it’ll get HUGE (mostly because it actually supports Intl
with every language pack which is kinda reeeally big). So I ended up with specifying es6
and default-3.6
as the features config. That’s working great and supports everything that I care to support.
Caching
This one’s a bit interesting. So that shouldCacheAggressively
is a bit dangerous, so here’s what I do… Because we’re server-rendering the page, I can actually generate the URL for the polyfill. It ends up looking like this (for IE 11):
polyfill.js?v=2&ua=Mozilla%2F5.0%20(Windows%20NT%2011.0%3B%20WOW64%3B%20Trident%2F7.0%3B%20rv%3A11.0)%20like%20Gecko
There are two query strings on there: v
which is associated to a version
that I have hard-coded. This allows me to break the cache in the event of an emergency if we need to change the config or something.
I also generate it with the ua
which is the user agent as part of the query string for the polyfill.js
file. Remember how I mentioned earlier that I allow the uq
query string to override req.headers['user-agent']
? So that’s what this is doing. The reason I do this is for caching. With such a specific URL, I can safely cache this forever. If the user upgrades (or downgrades!?) their browser, but the cache isn’t deleted, then this URL is changed and the old cached version isn’t used.
Extras
One “fun” experience I had while building this involved polyfill-service not playing nice with the way that babel transpiles classes. Follow that twitter thread and github issues linked for a “fun” time of your own… š
Conclusion
I’m excited about this and I’m hoping to build a more official polyfill service for more of PayPal applications to use it so folks can build applications with the latest JavaScript features without worrying about whether older browsers natively support what they’re writing and without making users of modern browsers pay a “tax” for users of older browsers.
Best of luck to you!
Looking for a job? Looking for a developer? Check out my job board (two remote positions and one in Portland, OR right now!): kcd.im/jobs
Learn more about JavaScript from me:
- More than you want to know about ES6 Modules @ Learn to Code Websites and Apps Meetup (remote)
- ES6 and Beyond Workshop Part 1 at PayPal (Jan 2017)
- ES6 and Beyond Workshop Part 2 at PayPal (March 2017)
- Code Transformation and Linting
- Writing custom Babel and ESLint plugins with ASTs
Also, don’t forget to subscribe to my youtube channel for my daily devtips, like the one a while back where I demo some advanced features of destructuring!
Things to not miss:
express-async-errors
- a nice package that allows me to useasync/await
on express route handlers/middleware without worrying about rejected promises being ignored and making my server hang :)- ReactJS Denver: Confidently Testing React Apps - A great talk at React Denver by Matt Parrish.
- React is no abstraction, React is JavaScript - A very interesting and important blog post from Robin Wieruch that teaches some common JavaScript fundamentals that you’ll need when working with React.
- The Surface Book 2 is everything the MacBook Pro should be by Owen Williams - I’m getting more and more convinced that my next computer is NOT going to be a MacBook. Here’s hoping that the Surface Book can get me 32 GB of RAM before I need another computer!
Some tweets from this last week:
> I just spent 2 hours working on upgrading all dependencies in paypal-scripts (a toolkit that dozens of teams are working on). > > If you have more than one project at your company, consider building a toolkit. I just saved ~48 hours of engineering time. Tools without config š š¦ ā 19 Jul 2018
> Checkout nbd as an alternative to node –inspect-brk or your editor debugger. Looks really awesome! https://github.com/GoogleChromeLabs/ndb ā 21 Jul 2018
> Did you know that you can make PRs to GitHub repositories using CodeSandbox!? ā 20 Jul 2018
This week’s blog post is “TODO”. It’s the published version of my newsletter from 2 weeks ago. If you thought it was good, go ahead and give it some claps (šx50) and a retweet:
Special thanks to my sponsor Patreons: Hashnode
P.S. If you like this, make sure to subscribe, follow me on twitter, buy me lunch, support me on patreon, and share this with your friends š
š Hi! Iām Kent C. Dodds. I work at PayPal as a full stack JavaScript engineer. I represent PayPal on the TC39. Iām actively involved in the open source community. Iām an instructor on egghead.io, Frontend Masters, and Workshop.me. Iām also a Google Developer Expert. Iām happily married and the father of four kids. I like my family, code, JavaScript, and React.