ShrinkRay All Articles
Developer Productivity

Your Dependency Tree Is a Haunted House: Ghosts, Duplicates, and Packages Nobody Invited

By ShrinkRay Developer Productivity
Your Dependency Tree Is a Haunted House: Ghosts, Duplicates, and Packages Nobody Invited

Open your package.json and count the entries under dependencies and devDependencies. Got maybe 50, 60 entries? Feels manageable, right? Now run npm ls --all | wc -l and watch that number explode into the hundreds — sometimes thousands. That's not a dependency list. That's a dependency forest, and most teams have absolutely no idea what's living in it.

This is the hidden cost of modern JavaScript development: every package you install brings its own party guests, and those guests bring their own guests, and suddenly your node_modules folder weighs more than a small database backup. The real problem isn't just disk space. It's build time, CI pipeline costs, bundle size, and security surface area. All of it compounds silently.

The Transitive Dependency Problem, Explained Simply

When you add a library like lodash or axios, you're not just adding one package. You're accepting responsibility for everything that package depends on, and everything those packages depend on. This is called the transitive dependency graph, and it's the reason a fresh React app can pull in 1,200+ packages before you've written a single line of code.

The math gets ugly fast. A project with 50 direct dependencies, each averaging 5 transitive dependencies with 3 levels of nesting, can realistically balloon to 500–1,000 unique packages. And that's before you account for version conflicts, where npm or yarn resolves the same package at multiple versions and installs all of them in parallel subtrees.

Version duplication is particularly sneaky. Run npm dedupe --dry-run on a mid-sized project and it's common to find 15–30 packages installed two or three times at slightly different versions. You're shipping redundant code, running redundant tests, and paying for redundant CI compute — all for packages that could have been unified under a single version.

How to Actually Audit Your Dependency Tree

The first tool every developer should know is npm ls. It prints the full dependency tree in your terminal. Pipe it through grep to find specific packages and see how many times they appear. It's ugly, but it works.

For something more visual, Dependency Cruiser (dependency-cruiser) lets you generate graphs of your internal module dependencies and flag circular references. It won't solve the npm bloat problem directly, but it's invaluable for understanding which of your own modules are pulling in what.

Bundlephobia (bundlephobia.com) deserves a permanent tab in your browser. Paste any npm package name and it'll show you the minified + gzipped size, the download time on different connection speeds, and — crucially — whether the package is tree-shakeable. If it's not tree-shakeable, you're importing the whole thing even if you only use one function.

For CI integration, size-limit is the move. Drop it into your pipeline and set hard limits on bundle size. When a PR pushes you past the limit, the build fails. Simple, enforceable, and it catches bloat before it merges instead of after it ships.

The Version Pinning Trap

Here's a pattern that bites teams constantly: well-intentioned developers pin dependency versions to avoid breaking changes ("lodash": "4.17.21" instead of "^4.17.21"). The intention is stability. The result is that automated tools like Dependabot and Renovate stop updating those packages, version drift accumulates, and six months later you're running three different versions of the same utility library across your monorepo because different packages pinned to different exact versions.

The smarter approach is to use caret ranges (^) for most packages, run a regular npm outdated audit, and lean on your lockfile (package-lock.json or yarn.lock) for reproducibility. The lockfile gives you exact version pinning at install time without forcing your package.json to be rigid about it.

Cutting the Fat: Practical Moves

Audit for replaceable giants. Moment.js is the canonical example — a 67KB (minified) date library that most projects use for exactly two or three operations. Replace it with date-fns (tree-shakeable, import only what you use) or the native Intl API and recover that space immediately.

Check for functionality you already have. Before installing a new utility package, search your existing dependencies. If you're already using Lodash, you probably don't need underscore. If you're on React 18, you might not need that separate state management library you installed back in 2021.

Use depcheck religiously. This CLI tool scans your codebase and identifies packages listed in package.json that aren't actually imported anywhere. In established codebases, it's not unusual to find 10–20 packages that got added for a feature that was later removed. Dead weight, still installed, still being processed by your bundler.

Prefer native over polyfill. A lot of projects carry polyfills for browser APIs that have had 95%+ support for years. Check your browserslist config. If you're targeting modern browsers, you might be shipping polyfills to users who haven't needed them since 2019.

The Build Time Multiplier Nobody Talks About

Every package in your tree is a file that gets read, parsed, and potentially processed during your build. TypeScript has to check types across all of it. Webpack or Vite has to resolve module graphs through all of it. Your CI runner has to cache all of it.

Teams that do a serious dependency audit — removing redundant packages, deduplicating versions, replacing bloated libraries with leaner alternatives — routinely report 20–40% reductions in cold build times. On a team running 50 CI builds a day, that's real money, real developer time, and real feedback loop improvements.

The haunted house metaphor holds up: you built a perfectly reasonable house, but over time, ghosts moved in. Old packages that nobody uses, duplicates lurking in the walls, zombie dependencies that exist only because something else needed them three versions ago. The cleanup isn't glamorous work, but it's some of the highest-leverage optimization you can do without changing a single line of application code.

Start with depcheck. Run npm dedupe. Spend 20 minutes on Bundlephobia for your top 10 dependencies. You'll be surprised what you find.