articles / Utility Libraries vs. Modern JavaScript

Utility Libraries vs. Modern JavaScript

Utility Libraries vs. Modern JavaScript

For years, almost every JavaScript project started by adding a general-purpose utility library. These collections of small helpers smoothed over an inconsistent standard library and patched browser differences. They were genuinely useful. But the language and the browser have moved on, and a large share of what those libraries provided is now built in. Carrying them by reflex adds weight for little gain.

What the language absorbed

Many one-line helpers that justified a dependency are now native methods. Arrays gained map, filter, reduce, find, includes, and flat. Objects gained Object.keys, Object.entries, and Object.assign. The spread syntax handles copying and merging that once needed a helper.

// once a library call, now native
const active = users.filter((u) => u.active);
const names = active.map((u) => u.name);
const merged = { ...defaults, ...overrides };
const unique = [...new Set(values)];

The native versions are standardized, well documented, and ship with the runtime, so there is nothing to install or keep updated.

Where a focused utility still helps

Some problems remain genuinely hard and are worth a dependency, as long as it is a focused one rather than a giant catch-all. Date and time handling is the clearest example: time zones, formatting, and arithmetic are full of edge cases that a small, well-tested library handles correctly. Deep equality, immutable updates to nested data, and high-precision decimal math are similar. The key shift is from one large library that does everything to a small package that does one thing well.

Prefer a library you can import piece by piece. Pulling a single function from a tree-shakeable package adds a few hundred bytes; importing the whole toolkit for that one function can add tens of kilobytes.

Counting the cost

Every dependency is code that ships to users, code that can contain bugs, and code that must be kept current. A helper that saves three lines but adds twenty kilobytes to every page load is a poor trade. Before adding one, it is worth checking whether a native method already does the job, and whether only a small slice of the library is actually needed.

// import only what is used, not the whole toolkit
import groupBy from 'lodash-es/groupBy';
// rather than: import _ from 'lodash';

Polyfills, the other kind of helper

A polyfill is different from a utility library. Rather than adding new convenience functions, it supplies a standard feature to an environment that lacks it, so the same modern code runs on older browsers. As support for a feature becomes near-universal, its polyfill becomes dead weight that should be removed.

The honest way to decide what is needed is to set a list of supported browsers for the project and let tooling work out the rest. A browser-support configuration drives both the polyfills that get added and the syntax a build step down-compiles, so the decision is data-driven rather than a guess. Shipping polyfills for browsers no one in the audience uses is a quiet but real cost on every page load.

// project browser targets, read by build + polyfill tooling
"browserslist": [
  "> 0.5%",
  "last 2 versions",
  "not dead"
]

A simple rule

Reach for the platform first. If a native method covers the case, use it. If the problem is genuinely thorny, choose the smallest focused package that solves it and import only the parts in use. Revisit those choices now and then, because a dependency that was essential two years ago may now duplicate something the language gained. That keeps a project light without reinventing the hard things, and it ages well because the native core does not need maintenance.

Common questions

Do I still need a general-purpose utility library?

Often not. Many helpers that justified one are now native array, object, and spread features. A focused library still earns its place for genuinely hard problems like date handling or decimal math.

What problems are still worth a dependency?

Date and time handling, deep equality, immutable updates to nested structures, and high-precision math are all full of edge cases. A small, well-tested package for those is usually a better choice than rolling your own.

How do I avoid shipping a whole toolkit for one function?

Import the single function from a tree-shakeable package, for example a per-function import path, rather than importing the entire library. This keeps the bundle small.

How do I decide whether to add a library at all?

Check first whether a native method already covers the case. If it does, use it. If not, weigh the bytes shipped and maintenance cost against the lines saved, and pick the smallest focused option.