Modern iterations of JavaScript have introduced some nice methods that make writing code a lot more legible, performant, and fun to write. Take, for example, the find()
method on the Array
prototype, which allows you to elegantly retrieve the first item in an array that meets some condition.
const players = [
{id: 3, name: "Bob"},
{id: 9, name: "Bill"},
{id: 2, name: "Baker"},
{id: 4, name: "Bo"},
];
const player = players.find(p => p.id === 9);
// {id: 9, name: "Bill"}
Features like this are slick, so it’s a bummer when they’re not supported by your target browsers. In those situations, it’s tempting to reach for the closest polyfill you can find, npm install
, and press forward. But if you’re striving to keep your bundle size as slim as possible, your best option might be to write a utility function instead.
Polyfills Can Be Fat
In many (if not most) cases, polyfill authors aim to keep their packages as close to the official specification as possible, or attempt to bridge the slight differences in how various browsers implement that feature. This makes sense — they’re written to be distributed, and so they need to behave predictably and consistently regardless of how a consumer chooses to implement them.
Consider that find()
method. It sounds simple, but with most of the polyfills out there (that I was able to find), you get a lot more than what you might expect. The one provided by MDN, for example, makes up 1,327 bytes:
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) {
// 1\. Let O be ? ToObject(this value).
if (this == null) {
throw TypeError('"this" is null or not defined');
}
var o = Object(this);
// 2\. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3\. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw TypeError('predicate must be a function');
}
// 4\. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
// 5\. Let k be 0.
var k = 0;
// 6\. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}
// 7\. Return undefined.
return undefined;
},
configurable: true,
writable: true
});
}
And from what I can find, that’s a pretty common thing. The Array.prototype.fill()
polyfill weighs in at about 928 bytes, Array.prototype.findIndex()
comes in at 1,549 bytes, and Array.from()
sits at 2,665 bytes.
This may not be the case for every polyfill from every author, but the trend appears to be pretty clear: these tools are intentionally built do cover a lot of bases required for any given specification, and that means you end up shipping more code than what your specific circumstance requires.
You Might Not Need the Full Specification
When you don’t need the full scope of what a polyfill entails, you can shave some bundle weight by rolling something more specific to you. And depending on the method, it often doesn’t take much. Gander at these few examples from methods I fairly commonly use:
A Simple Array.prototype.find()
Utility
Looking at find()
once again, a suitable helper method might look like this:
const find = (arr, func) => {
for(let index = 0; index < arr.length; index++) {
if(func.call(this, arr[index], index)) {
return arr[index];
}
}
return undefined;
}
const players = [
{id: 3, name: "Bob"},
{id: 9, name: "Bill"},
{id: 2, name: "Baker"},
{id: 4, name: "Bo"},
];
const player = find(players, p => p.id === 9);
// {id: 9, name: "Bill"}
A Simple Array.prototype.findIndex
Utility
And that could be easily converted into a findIndex()
utility as well:
const findIndex = (arr, func) => {
for(let index = 0; index < arr.length; index++) {
if(func.call(this, arr[index], index)) {
return index;
}
}
return undefined;
}
const players = [
{id: 3, name: "Bob"},
{id: 9, name: "Bill"},
{id: 2, name: "Baker"},
{id: 4, name: "Bo"},
];
const player = findIndex(players, p => p.id === 9);
// 1
A Simple Array.from()
Utility
If you’re simply looking to convert something like a NodeList
into an array, you could use something like this, which in this case, performs virtually the same function as Array.from()
:
const arrayFrom = (arrayLikeThing) => {
return [].slice.call(arrayLikeThing);
}
arrayFrom(document.querySelectorAll('span'));
// [...array of nodes]
A Simple Array.prototype.fill()
Utility
And here’s how a simple utility method for fill()
might look:
const fill = ({array, value, start = 0, end = undefined}) => {
end = end ? end + 1 : array.length;
array.splice(
start,
end - start,
array.slice(start, end).map(i => value)
);
return [].concat.apply([], array);
}
fill({
array: [1, 2, 3, 4, 5],
value: "x",
start: 1,
end: 3
});
// [1, 'x', 'x', 'x', 5]
Again, none of these utilities serve as a straight-up replacement for what any of the native APIs provide, and they’re not intended to do everything a polyfill would do. But they get your job done, they’re light, and it’s reasonably straightforward to build them yourself.
What about ready-made utility libraries?
You might be thinking of something like Lodash here. Depending on your needs, that might be a suitable choice. Still, similar tradeoffs exist in choosing to leverage tools like this rather than whipping up a utility more unique to your needs. Libraries like Lodash are intended for wide distribution, and so the methods they provide often just do more than what your specific circumstances require.
For example, our findIndex
implementation was less than 10 lines of code. But in v4.6.0, Lodash’s version is 11 lines, and it also depends on a shared baseFindIndex
method, which accounts for another 11 lines.
So, if we’re still prioritizing bundle size, leveraging a library like this in place of a polyfill to a native feature might not do much to help us out.
Sometimes, a Polyfill Does Make Sense
This definitely isn’t a blanket prescription for how you should handle feature support for older browsers. Depending on the context, it might make perfect sense to include a polyfill (or even lean on a utility library) — or maybe nothing at all. A few scenarios come to mind:
- You’re writing a library to be distributed. If that’s the case, you might want to leave your code as-is and instead require consumers to polyfill themselves when needed. This is helpful because it’ll lessen package size for a majority number of people, while still providing a path forward for the minority. In fact, it’s the approach I take with TypeIt. I don’t include API polyfills for IE and older, but I do document which ones people will need to include themselves, should they need to support an older browser.
- You use a particular feature a lot. If it’s become a habit to leverage a given feature, and each context is slightly varied, it might make sense to pull in a comprehensive polyfill. That piece of code, albeit beefy, might cover more specification gotchas between each implementation, and may also make it easier to transition away from the polyfill when native browser support becomes adequate. Not to mention, the ergonomics of some of these APIs is really good, and it may be worth the efficiency gains in developers getting to lean into them.
- You practice differential serving. It’s possible to automatically polyfill based on your target browsers using tools like @babel/preset-env. If you’re automating it like this, it’s become a popular pattern to generate two separate bundles — one for modern consumers, and one for legacy. This way, most people get a slimmer bundle, and you can freely use certain features without worrying so much about the added bloat.
- Ain’t got time 4 dat. It takes time to roll a utility function, and when you do, there’s always the chance you’ll miss something that a polyfill might have covered for you. That makes for the potential to spin your wheels when there may have been a better ROI by simply pulling in that polyfill.
Whatever You Do, Mind Your Bundle.
Especially when so many resources are quickly available via npm install
, it’s easy to lose sight of what’s actually ending up in your bundle (and what that means for your users). So, no matter how you approach providing new-ish features to your application, do it with your production code in mind.
Thanks for reading!
(This is an article published at macarthur.me. [Read it online here](https://macarthur.me/posts/when-it-makes-sense-to-use-a-utility-function-instead-of-a-polyfill).)
Top comments (0)