Skip to main content

Stop Installing Libraries You Don't Need

·PkgPulse Team

TL;DR

The most dangerous command in JavaScript development is npm install. Every library you add is a security surface, a maintenance obligation, a potential breaking change, and extra bytes in your bundle. The question isn't "does this library solve my problem?" — it's "is the problem worth the cost of a dependency?" Most libraries added to projects are used for 5-10% of their API, provide one function you could write in 20 lines, or could be replaced by a built-in browser/Node.js API. Here's how to make the call.

Key Takeaways

  • Every dependency costs: bundle size + security surface + update overhead + breaking change risk
  • The 80/20 rule: most packages are used for 20% of their API (or less)
  • The "Could I write this in 30 minutes?" test — if yes, consider it
  • Native alternatives exist for many commonly-installed packages
  • The packages worth installing: complex algorithms, battle-tested security, rich APIs you actually use

The Real Cost of a Dependency

Installing lodash costs you:

1. Immediate costs:
   → 71KB gzipped added to your bundle (if not tree-shaken)
   → 350KB unpacked in node_modules
   → 5+ seconds added to CI install time
   → 28 packages in the dependency tree (lodash is actually zero deps, but many aren't)

2. Ongoing costs:
   → Security monitoring: lodash has had 3 high CVEs in 5 years
   → Update maintenance: someone has to review lodash upgrades
   → Breaking changes: lodash v5 will have breaking changes
   → Lock-in: "we use lodash everywhere" makes future decisions harder

3. Opportunity cost:
   → Every dep is cognitive load for new developers
   → "Why do we use lodash here instead of Array.flat()?"
   → Onboarding slower, codebase harder to understand

4. The supply chain risk:
   → Lodash is trustworthy (large team, active)
   → But you install lodash → you implicitly trust all future lodash maintainers
   → Events like protestware, account takeover, typosqatting all exploited this trust

The decision calculus:
Is what this library gives me worth all of the above?
For lodash: often no (modern JS covers 80% of it)
For TanStack Query: yes (replaces 500 lines of custom fetching logic)
For Zod: yes (type-safe validation is complex to build well)

The Test Before Every npm install

# Ask these 5 questions before installing:

# 1. Does a native browser/Node.js API do this?
# Examples:
# fetch() → replaces node-fetch
# crypto.randomUUID() → replaces uuid
# Array.prototype.flat() → replaces _.flatten
# new URL() → replaces url.parse()
# structuredClone() → replaces _.cloneDeep
# Optional chaining (?.) → replaces _.get(obj, 'path.to.value')

# 2. Can I write this in under 30 minutes?
# If yes, write it. Own the code. No external dependencies.
#
# "I need to debounce a function"
function debounce<T extends (...args: unknown[]) => unknown>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
# 8 lines. No lodash. No package. No security surface.

# 3. Is this problem complex enough to justify someone else's solution?
# YES: cryptography, parsing algorithms, UI components with accessibility
# NO: basic array operations, simple string manipulation, one-off utilities

# 4. What's the package's health/activity?
npm view package-name | grep -E "latest|maintainers|license"
# Last published > 2 years ago: risk
# 1 maintainer, no recent activity: risk
# 0 downloads growth: ecosystem may be abandoning it

# 5. What does it actually cost?
npx bundlephobia package-name
# If it's 1KB and does 50+ things: install it (nanoid, ms, clsx)
# If it's 300KB and you use 1 function: don't (moment for formatting 1 date)

Packages Commonly Installed But Often Unnecessary

// Case 1: axios (11KB gzipped)
// "I need to make HTTP requests"
import axios from 'axios';
const data = await axios.get('/api/users').then(r => r.data);

// Native alternative:
const response = await fetch('/api/users');
const data = await response.json();
// When to still use axios:
// → Need request cancellation with AbortController? (fetch does this now)
// → Need interceptors for auth tokens? (fetch + wrapper does this)
// → Honestly: ky (2.5KB) is a better axios alternative in 2026

// Case 2: classnames/clsx
// "I need conditional CSS classes"
import clsx from 'clsx';
const className = clsx('base', isActive && 'active', { disabled: !enabled });

// Native alternative (for simple cases):
const className = ['base', isActive && 'active', !enabled && 'disabled']
  .filter(Boolean).join(' ');

// When to use clsx: complex conditional classes (it IS tiny at 0.5KB)
// When to skip it: 2-3 simple conditions

// Case 3: lodash for one function
import { debounce } from 'lodash';
// → Write debounce yourself (8 lines, shown above)

import { groupBy } from 'lodash';
const grouped = groupBy(items, 'category');
// → Object.groupBy is now built-in (Chrome 117+, Node 21+)!

import { cloneDeep } from 'lodash';
// → structuredClone() built-in

import { merge } from 'lodash';  // Only legit use case for lodash
// Deep merge is legitimately complex. This is worth it.

// Case 4: is-* packages
import isEmail from 'is-email';  // 2KB
// → const isEmail = (s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
// → Or use a validation library you already have (Zod, Yup)

import isUrl from 'is-url';
// → try { new URL(str); return true; } catch { return false; }
// 1 line. No package.

Packages Absolutely Worth Installing

Some problems are hard enough that a library is always the right answer:

Cryptography:
→ bcrypt (password hashing) — DON'T write your own
→ jose (JWT signing/verification) — crypto is easy to get wrong
→ argon2 — modern password hashing with proper defaults

Form validation with type safety:
→ Zod (14KB) — runtime + compile-time validation, complex to replicate
→ The schema inference magic is genuinely valuable
→ Writing your own would be 200+ lines for equivalent safety

Complex date handling (before Temporal):
→ date-fns (tree-shakeable, tiny when used properly)
→ The edge cases in date arithmetic are non-obvious
→ Timezones especially: don't write your own timezone handling

HTTP clients with retry logic + auth:
→ ky (2.5KB) — fetch wrapper with retries, timeout, hooks
→ Writing retry/timeout/abort correctly has many edge cases

Rich UI interactions:
→ framer-motion for complex animations (physics simulation)
→ Radix UI for accessible UI primitives
→ These solve accessibility problems that are legitimately complex

Database ORMs:
→ Prisma/Drizzle for type-safe queries
→ The type generation is worth the dependency

The pattern:
→ If the problem has security implications: use a library
→ If the problem has complex edge cases (dates, parsing): use a library
→ If the problem is something you've implemented twice with bugs: use a library
→ If the problem is "I need to call map() and filter()": don't use a library

The Audit: Removing Dependencies You Don't Need

# Find unused dependencies (these should always be removed):
npx depcheck
# Reports:
# Unused dependencies:
#   * lodash
#   * moment
#   * old-package-we-forgot-about

# Find packages where you use <20% of the API:
# This requires manual code review, but grep helps:

# How many lodash functions do you actually use?
grep -rh "from 'lodash'" src/ | sort | uniq
# If result: 2-3 functions → inline them and remove lodash

# Find massive packages where you use one thing:
grep -r "import.*from 'moment'" src/ | wc -l
# If result: 3 imports → switch to dayjs or Temporal

# Check for native alternatives:
grep -r "from 'uuid'" src/ | head -5
# → Replace with crypto.randomUUID()

grep -r "node-fetch\|cross-fetch" src/
# → Remove, use native fetch (Node 18+)

# After removing:
npm uninstall lodash moment node-fetch uuid cross-fetch
npm install dayjs  # If you actually need date manipulation
npm run build  # See the bundle savings

The "Dependency Budget" Mental Model

Treat your dependencies like a budget you spend carefully:

Your budget: 50 production dependencies
(Average mature project has 20-50 direct production deps)

HIGH-VALUE spends (worth multiple "budget units"):
→ TanStack Query: replaces 500+ lines of custom caching logic
→ Zod: type-safe validation with TypeScript integration
→ Prisma/Drizzle: type-safe DB queries
→ Radix UI: accessible UI primitives
→ Stripe SDK: payment processing security

LOW-VALUE spends (often not worth it):
→ is-email, is-url, is-integer: 3 lines of code
→ lodash (for 1-2 functions): write them yourself
→ uuid (in Node 18+): crypto.randomUUID()
→ node-fetch (in Node 18+): native fetch
→ classnames: Array.filter().join()

ZERO-VALUE spends (always remove):
→ Packages you're not using (depcheck finds these)
→ Packages whose functionality is now native
→ Old polyfills for ES features you're now targeting natively

The goal: every dependency should be intentional.
Not "we needed it once" but "we use this throughout, it's worth the cost."

Analyze any npm package's bundle impact at PkgPulse.

See the live comparison

View pnpm vs. npm on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.