Audit Log – 03:42 AM. I’ve been staring at the dependency graph for six hours. It looks like a bowl of digital spaghetti, and I’m the one who has to eat it.
$ du -sh node_modules
2.8G node_modules
$ npm build
> [email protected] build
> vite build
vite v4.3.9 building for production...
transforming (2842) ../../node_modules/lodash/lodash.js
<--- Last few GCs --->
[58210:0x60b0000] 142051 ms: Mark-sweep 2041.2 (2050.2) -> 2038.5 (2050.5) MB, 1402.1 / 0.0 ms (average mu = 0.125, current mu = 0.012) allocation failure; scavenge might not succeed
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
1: 0x1012e45a0 node::Abort() (.cold.1)
2: 0x1000a5c50 node::FatalError(char const*, char const*)
3: 0x1000a5d70 node::OnFatalError(char const*, char const*)
...
Aborted
To: Engineering Board
From: Senior Performance Auditor (Recovery & Disposal)
Subject: Technical Debt Autopsy – Project “Hydra”
Table of Contents
1. The Dependency Abyss
We aren’t building software anymore; we’re just piling up scrap metal and hoping the foundation doesn’t buckle. The current package.json is a crime scene. I found three different versions of react floating in the dependency tree. We have v16.14.0 for the legacy components, v17.0.2 for the “new” dashboard that was abandoned last year, and v18.2.0 for the current landing page.
This isn’t a codebase; it’s a geological record of every developer who wanted to put a new framework on their LinkedIn profile. We are shipping 1.2MB of compressed JavaScript to the client just to render a login page. That is the equivalent of building a 400-mile pipeline just to deliver a single glass of water. The sheer volume of javascript libraries we’ve invited into this house has turned our build process into a slow-motion train wreck.
Every time a developer runs npm install, they are essentially pouring concrete down the drains. We have is-buffer, is-number, and is-array scattered across the tree like discarded medical waste. Why are we paying engineers six-figure salaries to import a library that checks if a value is a number? The V8 engine is screaming, and I’m the only one listening.
2. Version Mismatch Carnage
The package-lock.json file is 45,000 lines long. It’s a suicide note written in JSON. When I ran npm dedupe, the system nearly caught fire. We have a recursive nightmare where [email protected] (which was deprecated when the dinosaurs still roamed) is being pulled in by five different “helper” utilities.
<<<<<<< HEAD
"lodash": "^4.17.21",
"axios": "^0.21.1",
=======
"lodash": "3.10.1",
"axios": "^1.4.0",
>>>>>>> feature/resume-driven-development
Look at that diff. Someone pinned lodash to v3.10.1 in a sub-module because they didn’t want to refactor a single _.map call, while the rest of the app is trying to move to v4.17.21. The result? We ship both. We are literally paying for the same code twice. This is structural engineering failure at its most basic level. If you build a skyscraper with two different grades of steel that don’t weld together, the building falls down. Our “building” is currently falling down every time a user on a mid-range Android phone tries to parse our 4MB main bundle.
The overhead of managing these javascript libraries is no longer a “cost of doing business.” It is a tax that has reached 100% of our productivity. We are spending 40% of our sprint cycles fixing “dependency hell” issues that shouldn’t exist in the first place.
3. The Tree-Shaking Lie
The marketing for modern build tools promises “tree-shaking” like it’s some kind of magical weight-loss pill. It’s a lie. You can’t tree-shake a septic tank. We are using CommonJS modules in 2024, which means Webpack and Vite just give up and include the whole bloated mess.
Take [email protected]. It’s a 700KB anchor strapped to our ankles. We only use it to format one date on the footer. Because it’s written in an ancient module format, the tree-shaker can’t strip out the 150 locale files we don’t need. We are shipping the Kazakh, Esperanto, and Ancient Sanskrit date formats to a user base that is 99% based in North America.
$ npm list moment
└─┬ [email protected]
└── [email protected]
When I look at the V8 heap snapshots, I see moment objects clogging the pipes like fatbergs in a London sewer. The garbage collector is working overtime, thrashing the CPU at 15% just to clean up the mess left behind by these “convenient” javascript libraries. We are trading user battery life for developer laziness. It’s a disgusting trade.
4. The Runtime Tax and V8 Exhaustion
Let’s talk about the V8 engine. It is a marvel of engineering, but it is being treated like a garbage disposal. Every time we load one of these “comprehensive” (to use a word I hate) javascript libraries, V8 has to parse, compile, and execute it.
I took a heap snapshot during the initial load. The results are nauseating. We have 42MB of “Strings” in memory before the user even clicks a button. Why? Because [email protected] and [email protected] are both loaded, each bringing their own massive validation schemas that are never used.
The cost of context switching in the V8 engine is non-trivial. When the engine tries to optimize our code, it looks for “hidden classes.” But because we use “utility” libraries that pass around generic objects with varying shapes, the JIT compiler gives up. It de-optimizes back to baseline code. We are essentially driving a Ferrari in first gear because the road is covered in the trash of our own making.
I spent three hours analyzing the impact of bn.js—a big number library—that was pulled in as a sub-dependency of a sub-dependency of a library we use to generate UUIDs.
[email protected] is a masterpiece of over-engineering for our needs. It’s designed for heavy-duty cryptography. We are using it to generate a string for a CSS ID. The memory overhead of the BN constructor, when called in a loop during list rendering, creates a spike in the Young Generation heap that triggers a Scavenge GC every 200ms. This is why the UI stutters. This is why the “smooth” scrolling feels like dragging a brick across sandpaper.
5. The “Javascript Libraries” Bloat: A Case Study in Failure
We need to address the elephant in the room: the blind worship of javascript libraries. We have a generation of developers who don’t know how to use the URL API, so they install query-string. They don’t know Array.prototype.flat(), so they pull in array-flatten.
Let’s look at [email protected]. It’s a fine library if you’re running in Node 10. But we are targeting modern browsers. The native fetch API is built-in, faster, and doesn’t require a 30KB payload. Yet, we have wrapped axios in a “service layer” that is so convoluted it makes a Rube Goldberg machine look like a straight line.
// The "Service Layer" Rot
import axios from 'axios'; // v1.4.0
const client = axios.create({ ... });
// 500 lines of interceptor logic that duplicates native browser behavior
This “abstraction” is a leaking pipe. It hides the underlying network logic, making it impossible to debug why a request is hanging. I found a memory leak in the interceptor chain where we were capturing the entire response object in a closure that never cleared. Over a 10-minute session, the heap grows by 150MB. That’s not a feature; that’s a slow-motion crash.
The reliance on these javascript libraries has atrophied the basic problem-solving skills of the team. When faced with a task, the first instinct is npm install, not “how does the platform solve this?” We are building on sand, and the tide is coming in.
6. The Build Pipeline Collapse
Our build pipeline is a structural failure. It takes 12 minutes to run a production build on a 16-core CI runner. This is an embarrassment.
[04:12:01] Initializing Vite build...
[04:14:22] Transforming modules... (this is where we die)
[04:21:45] Building chunks...
[04:24:10] Minifying bundles...
The “Transforming modules” phase is where the “resume-driven development” chickens come home to roost. Because we have so many overlapping javascript libraries, the transpiler has to process 4,000+ files. Most of these are just Babel helpers being injected into every single micro-module.
We are transpiling async/await into generator functions, then transpiling those generators into ES5 state machines, then polyfilling the polyfills. We are shipping code that would run on a browser from 2012, even though our analytics show 98% of our users are on Chrome 110+. We are intentionally making the app slower for everyone to accommodate a hypothetical user who doesn’t exist.
The “Tree-Shaking Lie” mentioned earlier is compounded by our use of babel-plugin-lodash. It’s a band-aid on a gunshot wound. It tries to rewrite imports on the fly, but it fails when it encounters our “custom” utility wrappers. The result is a build process that is both slow and ineffective.
7. The Septic Tank: A Deep Dive into ajv and lodash
Let’s look at the specific memory cost of our validation logic. We use [email protected]. It’s a high-performance JSON schema validator. In isolation, it’s great. But we’ve implemented it by re-compiling the same 50 schemas on every page load.
In V8, compiling a schema into a function is an expensive operation. It triggers the optimizing compiler (TurboFan). When you do this 50 times on boot, you’re essentially redlining the CPU. I’ve seen the main thread blocked for 1.2 seconds just “preparing” the validators.
And then there’s lodash. We aren’t even using the modular version. We are importing the whole lodash object in 40 different files.
import _ from 'lodash'; // The death knell of performance
Every time this happens, the bundler has to ensure the entire library is available. Even with deduplication, the internal references within lodash create a massive web of dependencies that make it impossible for the engine to prune dead code. We are carrying around 500 functions we don’t use so that someone could use _.get() instead of the native optional chaining operator (?.). It’s professional negligence.
The cost of these javascript libraries isn’t just the bytes; it’s the cognitive load. A new developer has to learn our specific “flavor” of Lodash, our specific “wrapper” for Axios, and our “custom” implementation of a React-Select fork that hasn’t been updated since 2019.
Final Recommendation
This codebase is a structural hazard. It is a series of leaking pipes held together by duct tape and “hope.” If we continue on this path, the “Project Hydra” will collapse under its own weight before the end of Q3.
My recommendation is brutal, but necessary:
- The Great Purge: Delete
lodash,moment, andaxios. Replace them with native ES2022+ features. If a developer can’t write afilterorreducewithout a library, they need to be put on a performance improvement plan, not given more dependencies. - Dependency Freeze: No new javascript libraries are to be added without a 10-page justification and a full audit of their sub-dependencies. If it adds more than 5KB to the bundle, the answer is “No.”
- Module Migration: Force-convert the entire project to ESM. If a library doesn’t support ESM, find one that does or write the functionality ourselves. We are done with the CommonJS dual-package hazard.
- Version Consolidation: We will use one version of React. One version of any library. If a sub-module breaks, we fix the sub-module or we kill it. No more
npm install --force. - Delete 50% of the Code: I have identified 12 “features” that have zero hits in our analytics over the last six months. These are just ghost rooms in our house, filled with old javascript libraries that are still being loaded and parsed. Burn them down.
We are currently building a landfill. I suggest we start building software instead.
Signed,
The Auditor
05:12 AM – The sun is coming up, and the node_modules folder is still 2.8GB. God help us all.
Related Articles
Explore more insights and best practices: