Run npm install on almost any JavaScript project and you’ll pull in hundreds of packages you’ve never heard of. Some of them do real work. A surprising number exist to check whether a value is a string, to provide a feature every supported runtime has had for a decade, or to wrap a single line of code. Each one is small, but together they slow installs, bloat bundles, and widen your supply-chain attack surface.
In March 2026, James Garbutt, an open-source contributor involved with the e18e (“ecosystem performance”) community, wrote an essay called “The Three Pillars of JavaScript Bloat” that explains where much of this comes from. It’s worth understanding, because once you see the three causes, you start spotting them in your own dependency tree.
Pillar 1: Support for very old runtimes
A lot of widely used packages were written when JavaScript engines were far less capable and far less consistent. Code had to work in ES3-era engines like old Internet Explorer, and in edge cases like values passed between iframes (different “realms”), where checks such as instanceof Array give the wrong answer. Some authors also wanted protection against other code tampering with built-in globals.
Library authors handled this defensively, which was reasonable at the time. The result is a family of tiny packages such as is-string and hasown that do simple checks in a way that works in every possible environment.
Garbutt’s point is that these are valid for a small number of projects that really need them, but everyone else pays the cost. If your code will never run on an engine from 2012, you’re paying for compatibility you’ll never use. In most modern code, typeof x === "string" or Object.hasOwn(obj, key) does the job.
Pillar 2: Atomic architecture
For a while, the JavaScript community embraced very small packages, sometimes a single function each, as a design philosophy. The idea was appealing: reuse everything, write nothing twice, and let each module do one thing well. The essay’s examples include arrify (wrap a value in an array), slash (convert backslashes to forward slashes) and shebang-regex (a single regular expression).
In practice, Garbutt notes, many such packages end up with only one real consumer, or get duplicated at several versions across a dependency tree. The 2016 left-pad incident, when removing one tiny package from npm broke builds across the ecosystem, was the most famous warning about this. The costs haven’t gone away:
- Install time and disk use: every package means another tarball to download and unpack.
- Security exposure: every maintainer account is a possible point of compromise. Several recent npm supply-chain attacks spread through small, widely depended-on packages.
- Duplication: different parts of the tree often pull in different versions of the same helper.
The alternative isn’t to never use dependencies. It’s to be more willing to write a three-line function yourself, and to prefer packages that do a complete job over chains of micro-packages.
Pillar 3: Ponyfills that overstayed their welcome
A polyfill adds a missing feature to the environment. A ponyfill provides the same feature as a separate function, without touching globals. Both were essential while features like globalThis, Array.prototype.indexOf or Object.entries were new or unevenly supported.
Those features are now supported in every current browser and Node.js release. Yet packages like globalthis, indexof and object.entries are still depended on by popular libraries and still downloaded in large numbers. Nobody removed them because nothing broke. They just stayed.
How to find bloat in your own project
Knowing the three causes is useful. Actually doing something about them is better. These tools help:
npm lsandnpm explain <package>: built into npm.npm explaintells you why a package is installed and what depends on it.- npmgraph: visualises a package’s dependency tree, which makes deep chains of tiny packages easy to spot.
- The e18e CLI: analyses your dependencies and suggests native replacements or lighter alternatives.
- module-replacements: a community-maintained list of packages that can be replaced with native features or lighter alternatives. There’s also an ESLint plugin that flags them in your code.
- Knip: finds unused dependencies, files and exports in a project. Unused dependencies are the easiest bloat to remove.
- Bundle analysers: for front-end code, tools such as
rollup-plugin-visualizer,webpack-bundle-analyzeror Vite’s build output show what actually ends up in the browser.
Fixing it: practical steps
Replace what the platform already does
Many small packages map directly onto built-ins. A few common examples:
// Instead of a deep-clone package, for plain data
const copy = structuredClone(original);
// Instead of an "is-array" style package
Array.isArray(value);
// Instead of hasown
Object.hasOwn(obj, "key");
// Instead of an object-entries ponyfill
Object.entries(obj);
// Instead of a promise-based sleep helper (Node.js)
import { setTimeout as sleep } from "node:timers/promises";
await sleep(500);
Check the module-replacements list before adding a new dependency. It often names the native equivalent directly.
Mark your package as side-effect free, if it is
If you publish a library, tell bundlers they can safely drop unused modules by setting sideEffects in package.json:
{
"name": "my-library",
"sideEffects": false
}
Only do this if importing your modules really has no side effects. If some files do, such as CSS imports or code that registers globals, list them instead: "sideEffects": ["*.css"].
Use leaner tooling
Tools like Biome combine linting and formatting in a single binary with few dependencies, which can replace a large stack of ESLint and Prettier plugins in some projects. In current versions of Biome, the command to apply safe fixes is biome check --write.
Upstream the fix
Most bloat isn’t in your code. It’s in your dependencies’ dependencies. Garbutt’s recommendation is to raise it with maintainers: open an issue or a pull request on the library that pulls in an unnecessary package. That’s what the e18e community has been doing, and those changes benefit every project that uses the library.
Set a policy for new dependencies
Ask a few questions before adding a package:
- Could this be done in a few lines of code, or with a built-in?
- How many transitive dependencies does it bring with it?
- Is it actively maintained, and by whom?
- Does it support runtimes we don’t need, at our expense?
Why it’s worth the effort
Dependency bloat rarely breaks anything, which is why it builds up. But it has real costs: slower CI, slower installs for every developer, larger bundles for users on slow connections, and more places for a supply-chain attack to start. Garbutt’s essay is a useful reminder that much of this weight comes from reasonable decisions made years ago that nobody went back to revisit.
You don’t need to audit everything at once. Run Knip, look at the biggest trees in npmgraph, and remove or replace a few packages each sprint. Over time, it adds up.
Related reading
- A Developer’s Guide to Abstract Syntax Trees
- Migrating from Zig to Rust: When It Makes Sense
- Code Review on GitHub: Setup and Habits That Work
Sources: James Garbutt, “The Three Pillars of JavaScript Bloat”, e18e, module-replacements, Knip, webpack tree-shaking guide, Biome CLI reference.