The Three Pillars of JavaScript Bloat

An analysis of why npm dependency trees are growing larger over time, focusing on legacy runtime support, safety mechanisms, and the pitfalls of atomic architecture.
The Three Pillars of JavaScript Bloat
Over the last couple of years, we’ve seen significant growth of the e18e community and a rise in performance focused contributions because of it. A large part of this is the “cleanup” initiative, where the community has been pruning packages which are redundant, outdated, or unmaintained.
One of the most common topics that comes up as part of this is “dependency bloat” - the idea that npm dependency trees are getting larger over time, often with long since redundant code which the platform now provides natively.
In this post, I want to briefly look at what I think are the three main types of bloat in our dependency trees, why they exist, and how we can start to address them.
1. Older runtime support (with safety and realms)
The graph above is a common sight in many npm dependency trees - a small utility function for something which seems like it should be natively available, followed by many similarly small deep dependencies.
So why is this a thing? Why do we need is-string instead of typeof checks? Why do we need hasown instead of Object.hasOwn (or Object.prototype.hasOwnProperty)? Three things:
- Support for very old engines
- Protection against global namespace mutation
- Cross-realm values
Support for very old engines
Somewhere in the world, some people apparently exist who need to support ES3 - think IE6/7, or extremely early versions of Node.js.1
For these people, much of what we take for granted today does not exist. For example, they don’t have any of the following:
Array.prototype.forEach
Array.prototype.reduce
Object.keys
Object.defineProperty
These are all ES5 features, meaning they simply don’t exist in ES3 engines. For these unfortunate souls who are still running old engines, they need to reimplement everything themselves, or be provided with polyfills.
Protection against global namespace mutation
The second reason for some of these packages is “safety”. Basically, inside Node itself, there is a concept of “primordials”. These are essentially just global objects wrapped at startup and imported by Node from then on, to avoid Node itself being broken by someone mutating the global namespace.
For example, if Node itself uses Map and we re-define what Map is - we can break Node. To avoid this, Node keeps a reference to the original Map which it imports rather than accessing the global. Some maintainers also believe this is the correct way to build packages, too.
Cross-realm values
Lastly, we have cross-realm values. These are basically values you have passed from one realm to another - for example, from a web page to a child <iframe> or vice versa. In this situation, a new RegExp(pattern) in an iframe, is not the same RegExp class as the one in the parent page. This means window.RegExp !== iframeWindow.RegExp, which of course means val instanceof RegExp would be false if it came from the iframe.
Why this is a problem
All of this makes sense for a very small group of people. The problem is that the vast majority of us don’t need any of this. We’re running a version of Node from the last 10 years, or using an evergreen browser. These layers of niche compatibility somehow made their way into the “hot path” of everyday packages. Instead, it is reversed and we all pay the cost.
2. Atomic architecture
Some folks believe that packages should be broken up to an almost atomic level, creating a collection of small building blocks which can later be re-used to build other higher level things. For example, shebang-regex is just a single regex export.
Why this is a problem
In reality, most or all of these packages did not end up as the reusable building blocks they were meant to be.
- Single use packages: Many granular packages are used almost solely by one other package by the same maintainer.
- Duplication: Inlining them doesn’t mean we no longer duplicate the code, but it does mean we don’t pay the cost of things like version resolution and conflicts.
- Larger supply chain surface area: The more packages we have, the larger our supply chain surface area is. Every package is a potential point of failure for maintenance and security.
Source: Hacker News









