What Your package.json Is Actually Carrying: An Afternoon Audit for Overgrown JavaScript Trees
A JavaScript project rarely becomes heavy through a single decision. It gains weight the way a garage does: a utility library here, a date formatter there, a build plugin that solved one awkward Tuesday in 2023 and has been installed ever since. None of those choices were unreasonable in isolation. The aggregate, though, is a tree that nobody on the team can describe from memory — and a tree you cannot describe is a tree you cannot defend when a maintainer disappears or a transitive package changes hands.
The point of the exercise below is not minimalism as an aesthetic. It is stewardship: knowing what you ship, why each piece exists, and what would break if it vanished. For a mid-sized application, the whole routine takes one focused afternoon, and it produces decisions rather than guilt.
Inventory Before Judgment
Most dependency conversations start with an opinion. Start with a count instead. Four commands will give you the shape of the tree in the time it takes to brew coffee:
npx depcheck # candidates for removal
[npm](https://en.wikipedia.org/wiki/Npm) ls --all --parseable | wc -l # total installed packages
npx source-map-explorer 'dist/assets/*.js' # what actually ships
npm audit --omit=dev # known advisories in runtime deps
Treat depcheck as a list of leads, not a verdict. It cannot see packages consumed only by configuration files, packages imported for side effects, or anything referenced inside a build tool. Every hit needs one grep through the repository before it earns a removal.
The Two Numbers That Actually Matter
The count of installed packages and the count of shipped bytes are different problems with different owners. Your node_modules can hold eleven hundred packages while the browser downloads two hundred kilobytes, because most of that tree never reaches a bundler. Conversely, a single careless barrel import can pull an entire library into the client bundle while the dependency count stays flat.
Keep the numbers separate. Installed packages are a supply-chain and CI-install-time concern. Shipped bytes are a user-experience concern. Conflating them produces arguments in which both sides are correct and neither is useful.
A Supply-Chain Pass for 2026
The ecosystem's hygiene expectations have risen considerably, and an audit that ignores them is only half an audit. Four checks cover most of the realistic exposure:
- Lockfile discipline. The lockfile is committed, reviewed, and regenerated only by intentional updates. A floating install in CI is a dependency you did not approve.
- Provenance and attestations. Packages published with a verifiable link between source repository and registry artifact deserve preference over those without, particularly for anything touching authentication, payments, or the build.
- Maintainer and publish signals. A package with one maintainer, no release in three years, and a recent burst of unrelated activity is worth a second look before the next upgrade.
- Install-time scripts. Post-install hooks execute arbitrary code on developer machines and CI runners. Enumerate them with
npm query ':attr(scripts, [postinstall])'and decide, package by package, whether each one is still earning that privilege.
None of this requires a security team. It requires a checklist and the willingness to run it on a schedule rather than during an incident.
Three Verdicts, Applied Honestly
Every package in the inventory should end the afternoon in exactly one of three states.
Remove. Unused direct dependencies, development tools that duplicate a capability already present, and packages that wrap twenty lines of standard library code. Removal is the cheapest verdict and the most frequently deferred, because deleting a dependency feels riskier than adding one. Cover the handler with a test first, then delete.
Replace. A package earns replacement when the platform has caught up or when the package's cost now exceeds its benefit. The classic case is a utility library kept alive for three functions that modern JavaScript provides natively. The second classic case is a legacy date library: with the Temporal API reaching modern engines, the calculus for any new date handling has shifted decisively toward the platform, and existing code can migrate incrementally behind a small wrapper.
Replacement has a hidden cost, and it is not bundle size — it is behavioral drift. A hand-rolled replacement that handles ninety-five percent of the old library's edge cases is a regression waiting for a leap year. Budget for the edges or keep the dependency.
Pin. Anything that is neither unused nor replaceable should be pinned deliberately. For runtime dependencies, that means an exact version plus a review process for upgrades, or at minimum a lockfile that CI installs from strictly. For transitive packages that need a temporary hold, the overrides field in package.json is the supported mechanism — and every override deserves a comment and an expiry date, because undocumented overrides outlive the people who wrote them.
Run It on a Calendar, Not on Impulse
The audit earns its keep when it becomes routine. Three lightweight additions turn it from an event into a practice:
- A bundle-size budget in CI that fails the build when shipped bytes cross an agreed threshold.
- A dependency-review step on pull requests that surfaces new packages and advisories before merge.
- A quarterly thirty-minute review of the inventory, run with the same four commands, producing a short written record of what changed and why.
The record matters more than the numbers. Six months from now, the question will not be how many packages exist — it will be why a particular one is still there. An audit that leaves that answer written down has done its job.
What This Audit Is Not
It is not a race to zero. Dependency count is a poor proxy for quality, and a team that strips libraries to hit a number will eventually reimplement them badly. The goal is a tree whose every branch can be named, explained, and retired on purpose. That is a lower and far more useful bar than purity — and unlike purity, it is achievable in an afternoon.