title: | Tips on gardening your dependencies |
---|---|
description: | How I actually wrangle the many tools that we use to build Val Town |
pubDate: | 2025-09-11:00:00.000Z |
author: | Tom MacWright |
Val Town is a React application with a ton of dependencies. It's complicated and
we have to deal with dependency upgrades all the time. We are committing the
cardinal sin of overcomplicating the web: our node_modules
directory is 863MB
as of this writing. Whew!
Is it, though? Are we installing dependencies freely, taking on technical debt left and right? I'd say not really.
The thing is, there's some
essential complexity in
what we're trying to build. We aren't going to DIY our own TypeScript transpiler
or avoid installing CodeMirror and use a textarea for
our code editing. I spent a little time every week looking through
package.json
and thinking which of these can I remove. Sometimes I find a
dependency that can be yanked, but a lot of times I come up empty-handed: we
actually need all this junk. My ability to judge other people fades as I learn
the hard way how principles make contact with reality.
But that's not to say that there's no art in dependency grooming. There are a bunch of techniques and tools that all fit together into a general sort of dependency hygiene that I've developed. I'm not sure I've written it down anywhere in full. Here's a shot.
Rule #1 is to read. This is very literal: read the source code of any dependency that you're about to introduce into your project. And, of course, the README. Don't ask your LLM to, don't be a baby, put on your spectacles and at least read the parts of the module that your application is going to rely on.
Fairly often you'll do this and discover that the new dependency you're adding is just 50 lines long, and is better off vendored rather than installed with NPM: just copy that code over and preserve its open source license in a code comment.
Or you'll realize that the module is 2MB gzipped and introduces 3 new transitive
dependencies, but you're only actually using 50 lines of it. Again, this is not
a good scenario: you're introducing a lot of surface area that's pure downside.
It'll take up more room in node_modules
, or it could have security
vulnerabilities in the parts that you aren't using and you'll have to triage
them just the same.
If you don't read, you won't succeed.
Or, if you're using pnpm
, pnpm-lock.yaml
and pnpm why
. And a similar
command for whatever other package manager you're using. The reason is pretty
simple: your direct dependencies are inevitably the tip of the iceberg. What
really fills up node_modules
is all of the stuff they bring along, and that
stuff - transitive dependencies - is extremely important.
For example, let's say your project needs to transpile TypeScript. It's fairly
likely that you already have a transpiler installed: in our project, we have
copies of esbuild that are dragged in by drizzle-kit,
by Vite, and by tsx.
So using esbuild
as a direct dependency costs us nothing: it gets deduped to
the same esbuild
binary that everything was using under the hood before. This
is a useful little game: when you're installing something for your application,
if you can find a way to reuse something that's already installed transitively,
you can get a dependency for free!
And read package-lock.json or pnpm-lock.yaml. It's not that bad, and you'll
learn something. There's a lot in there. It'll familiarize yourself with what
modules others rely on, which builds a little PageRank algorithm in your head so
that you can recall, off the bat, what you might want to use for something new.
Feed your curiosity and open those npmjs.com
webpages.
The NPM module ecosystem is made up of people. It's useful to know who those people are! For example, when I'm looking for something related to Promises (or many other topics), I'll check if Sindre Sorhus has already published something. Very often he has!
Other folks who have lots of solid work are good to know - isaacs, Matteo Collina, Mafintosh.
If you're doing something with Markdown, you should know all the wooorm and unified repos. Nextgen Node.js stuff? Check unjs. Transpiler internals, you should always check Rich Harris's projects, which include so many gems.
I don't know if there's a way to systematize this, and I would be cautious of one because incentives are usually subverted. It's more like learning the 'lay of the land' of the internet. I find it kind of fun. Maybe you will?
But what are you really looking for? The definition of a good module keeps shifting, but pretty often it'll look something like: a decent history of maintainership, built-in TypeScript types, passing tests, good documentation.
A slang shorthand for this would be that it should have a vibe of competence. Even if you're building something by the seat of your pants and making lots of mistakes, you want the parts that you're using to be solid. An application's bugs are the sum total of the bugs you write and the bugs you inherit from others, so it's actually kind of fair to have higher standards for code that you install than code that you write.
What makes a bad module? Of course something that's abandoned and poorly written is bad, but even worse than that is a module that solves the wrong problem - something that doesn't actually fit the problem you have and instead you have to shift the problem to make it work. You can fix this by reading and spending a little time understanding both your problem and the solution. Or asking the LLM, you little baby.
You should be using Renovate. It'll nag you to keep your modules up to date, and it's better to do that kind of work incrementally rather than in one yearly push.
And you should be using Knip. It's just straight-up magic:
it's so fast, extremely accurate. It'll tell you which modules you have
specified in package.json
but that you aren't actually using. It's super easy
to lose track and have a lot of junk lying around from old versions of a
project. Get rid of it with Knip! It'll even show you which files your project
no longer uses. If there was a Knip t-shirt I'd be wearing it right now, that's
how good it is.
That's the truth: we're all building on the shoulders of each other. But there's an art to finding the right shoulders to build on.
In a way, it's frustrating that the web platform, and the NPM module ecosystem, moves so fast and requires so many nitpicky updates and decisions. But that's pretty much the norm: even next-generation languages that have learned the lessons of NPM and Node suffer from bloated package ecosystems. Gardening dependencies is part of the job and should do it well.