Luca Mele
Luca Mele

Performance

Reduce Your Node Dependencies for Maintainable Apps

Reduce Your Node Dependencies for Maintainable Apps
Back to all posts
·7 min read

Run `ls node_modules | wc -l` in your project. If the number makes you uncomfortable, you're not alone. The average JavaScript project pulls in hundreds of transitive dependencies, and each one is a risk: security vulnerabilities, breaking changes, abandoned maintenance, or simply bloat.

After years of leading frontend teams at companies like AXA, Migros, Vontobel, and UBS, I've developed a strong opinion: every dependency should earn its place. If you can write it in 20 lines, you probably should.

The Hidden Cost of npm install

Dependencies aren't free. Each one comes with costs that don't show up in your bundle size analyzer:

Security surface area — every package is code you didn't write and can't fully audit. The event-stream incident, the ua-parser-js hijack, the colors.js sabotage — these aren't edge cases, they're the natural consequence of trusting thousands of strangers with your supply chain.

Maintenance burden — packages update, and those updates can break things. The more packages you have, the more time you spend on dependency updates instead of building features. I've seen teams spend entire sprints just getting through Dependabot alerts.

Cognitive overhead — new developers joining your project need to understand not just your code but every abstraction your dependencies introduce. That 'helpful' utility library with 200 functions means 200 things someone might use instead of the standard approach.

Build time impact — at Migros, we ran a PNPM monorepo with Turbo for Bikeworld and Micasa. When we audited our dependency tree and removed unnecessary packages, our CI pipeline got significantly faster. That's developer time saved on every single PR.

Before You Install, Ask Three Questions

I enforce a simple checklist in my teams before adding any dependency:

1. Can we write this ourselves in under 50 lines? If yes, write it. A small utility you own is better than a package you don't control. You understand it completely, it has zero transitive dependencies, and it does exactly what you need.

2. Is this package actively maintained? Check the last commit date, open issues, and bus factor. A package with one maintainer who last committed 8 months ago is a ticking time bomb.

3. What's the transitive dependency count? Install it in isolation and check what it pulls in. I've seen 'simple' packages bring in 40+ transitive dependencies. That's 40 packages that can break, get hijacked, or become abandoned.

# Check what a package actually pulls in
mkdir /tmp/dep-check && cd /tmp/dep-check
npm init -y && npm install <package-name>
ls node_modules | wc -l
# If this number surprises you, reconsider

Common Packages You Don't Need

Here are real examples from projects I've inherited and cleaned up:

lodash — Modern JavaScript has Array.flat(), Array.flatMap(), Object.entries(), Object.fromEntries(), structuredClone(), and the optional chaining operator. Import the specific lodash function if you truly need it (lodash/debounce), never the entire library.

moment.js — Use the native Intl.DateTimeFormat API, or at most date-fns with tree-shaking. Moment adds 300KB to your bundle and is officially in maintenance mode.

axios — The Fetch API is available everywhere now, including Node.js 18+. You don't need a wrapper around XMLHttpRequest anymore.

uuid — If you just need unique IDs, crypto.randomUUID() is built into every modern runtime.

classnames / clsx — If you're using Tailwind, you likely already have a cn() utility or can write one in 3 lines with template literals.

// Instead of: import { v4 as uuid } from 'uuid'
const id = crypto.randomUUID();

// Instead of: import axios from 'axios'
const res = await fetch('/api/data');
const data = await res.json();

// Instead of: import _ from 'lodash'
const unique = [...new Set(items)];
const grouped = Object.groupBy(items, (i) => i.type);

When Dependencies Are Worth It

I'm not saying avoid all packages. Some problems genuinely need external solutions:

Cryptography — never roll your own. Use established, audited libraries.

Complex parsing — date parsing with timezone awareness, CSV/XML parsing, markdown rendering. These are domains where edge cases will eat you alive.

Framework integrations — if you're using Next.js, React, or Vue, their ecosystem packages exist for good reasons. next/image optimizes images better than anything you'd write yourself.

At Vontobel, we kept our dependency count lean while building a complex financial platform with real-time WebSocket pricing. The key dependencies — React, Next.js, and a few specialized financial formatting libraries — all earned their place. Everything else, we wrote ourselves or used platform APIs.

A Healthier Approach

My rule of thumb: treat dependencies like hiring. Each one should go through a review process. Does it solve a genuinely hard problem (cryptography, PDF generation, image processing)? Keep it. Does it save you 15 minutes of writing a utility function? Write the function.

Run a dependency audit quarterly. Remove unused packages. Replace heavy packages with lighter alternatives or native APIs. Make it a team habit, not a one-off cleanup.

The result is a project that's faster to install, faster to build, easier to audit, and — most importantly — easier for your team to maintain long-term. Your future self will thank you when you're not debugging a production issue caused by a transitive dependency five levels deep that you didn't even know existed.