Skip to main content

The Hidden Cost of npm Dependencies

·PkgPulse Team

TL;DR

Every dependency is a liability disguised as a feature. That 2KB utility library you installed has 15 transitive dependencies, adds 47KB to your bundle when bundled naively, hasn't been updated in 8 months, and has a copyleft license clause you didn't read. The hidden costs of dependencies are real, measurable, and often ignored until they cause problems. This article gives you the framework to evaluate the full cost before npm install.

Key Takeaways

  • Average production app: ~1,000 transitive dependencies — you didn't choose most of them
  • Bundle cost — a package's actual bundle impact includes all its dependencies
  • Security surface — each transitive dep is a supply chain attack vector
  • License risk — GPL/LGPL licenses in transitive deps can affect your commercial code
  • Maintenance debt — abandoned packages silently accumulate CVEs
  • Breaking changes — every major dependency version update is a migration task

The Full Cost Model

When you npm install <package>, you're accepting:

Direct costs:
1. Bundle size impact (the package + its dependencies)
2. Install time (affects CI speed)
3. Learning curve (API, patterns, mental model)

Hidden costs:
4. Security surface (every dep is a potential supply chain attack)
5. License obligations (some deps have viral licenses)
6. Maintenance burden (breaking changes require code updates)
7. Transitive dependencies (the package's dependencies' dependencies)
8. Lock-in (migrating away later is always harder than expected)

Quantifying Hidden Costs

Cost 1: Bundle Size (With Dependencies)

# bundlephobia.com shows you the REAL bundle impact:

# What you think you're installing:
axios: "HTTP client, ~14KB gzipped"

# What actually gets bundled:
axios + follow-redirects + form-data + combined-stream + asynckit = ~32KB

# vs native fetch: 0KB (built into every modern environment)

# More examples:
moment.js: "72KB gzipped" — but the default import includes ALL locales
date-fns: "75KB gzipped" — but with tree-shaking: ~8KB for format + parseISO
lodash: "71KB gzipped" — but import { groupBy } from 'lodash-es': ~2KB

# The lesson: check bundlephobia before every install
// Dependency audit: check your actual bundle impact
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      filename: 'dist/bundle-stats.html',
    }),
  ],
});

// Run: vite build → opens interactive treemap
// Shows which dependencies dominate your bundle

Cost 2: Transitive Dependencies

# Visualize the dependency tree
npm ls --depth=3

# Count transitive dependencies
npm ls --json | jq '[.. | objects | .name? // empty] | length'

# Find which deps bring the most transitive deps
npx depcheck              # Check for unused dependencies
npx npm-why <package>     # Why is this transitive dep installed?

# Real example: create-react-app's node_modules
# CRA installs ~1,200 packages for a "hello world" app
# Vite equivalent: ~80 packages
# That's 1,120 packages you never asked for, each with its own security surface

Cost 3: Security Surface

# Measure your attack surface
npm ls | wc -l              # Count all packages (including transitive)
npm audit --json | jq '.metadata.vulnerabilities'
# Output: { info: 0, low: 2, moderate: 5, high: 1, critical: 0 }

# The relationship: more deps = more CVEs
# Real correlation from npm's data:
# Apps with 100-200 deps: average 2.3 vulnerabilities
# Apps with 500-1000 deps: average 11.7 vulnerabilities
# Apps with 1000+ deps: average 28.4 vulnerabilities

# Every transitive dep you add increases this number

Cost 4: License Risk

# Check licenses before installing
npx license-checker --summary

# Output:
# MIT: 847 packages
# ISC: 156 packages
# BSD-3-Clause: 89 packages
# Apache-2.0: 34 packages
# GPL-3.0: 2 packages   ← FLAG THIS
# Unknown: 5 packages   ← FLAG THIS

# GPL risk in commercial products:
# GPL-3.0 packages: if you distribute software including a GPL library,
#   your code may need to be GPL too (consult a lawyer)
# LGPL: more permissive, but dynamic linking rules are complex
# AGPL: includes SaaS/network use — very viral

# Most npm packages are MIT, ISC, Apache (permissive = safe)
# Flag and review any GPL, LGPL, AGPL, or unknown licenses

# Tools:
npx license-checker --onlyAllow 'MIT;ISC;Apache-2.0;BSD-3-Clause;BSD-2-Clause;CC0-1.0;Unlicense'
# Fails CI if any dep uses a non-permissive license

The Dependency Cost Calculator

Before installing any package:

# Step 1: Do I need this?
# Can I implement this without a dependency?
# Examples of "no dependency needed" in 2026:
#   - UUID: crypto.randomUUID() (Node.js 19.6+, browsers)
#   - Fetch: native fetch (Node.js 18+)
#   - Deep clone: structuredClone() (Node.js 17+)
#   - Debounce: 8 lines of code
#   - Flatten array: arr.flat()
#   - Remove duplicates: [...new Set(arr)]
#   - Get object values: Object.values(obj)

# Step 2: Check the real bundle impact
npx bundlephobia <package>

# Step 3: Count transitive dependencies
npm show <package> dependencies
# If it has > 5 direct deps, check bundlephobia for the real cost

# Step 4: Check health
# → pkgpulse.com/package/<package>
# → Last published? Maintained? CVEs?

# Step 5: Check license
npm show <package> license
# Anything not MIT/ISC/Apache-2.0/BSD? Research further.

# Step 6: Install decision:
# Small (< 5KB), no deps, MIT, maintained, needed → YES
# Large (> 50KB), many deps, abandoned → NO or find alternative

The One-Dependency Rule

For some categories, use zero dependencies:

// Replace these with native alternatives:

// UUID (before: uuid package, 2KB)
import { v4 as uuidv4 } from 'uuid';
uuidv4()
// After: native (0KB)
crypto.randomUUID()

// Deep clone (before: lodash.clonedeep, 9KB)
import cloneDeep from 'lodash/cloneDeep';
// After: native (0KB)
structuredClone(obj)

// Array shuffle (before: lodash.shuffle, 2KB)
// After: Fisher-Yates, 5 lines
function shuffle<T>(arr: T[]): T[] {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

// Debounce (before: lodash.debounce, 2KB; or debounce package)
// After: 8 lines
function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T {
  let timer: ReturnType<typeof setTimeout>;
  return ((...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  }) as T;
}

// String padding (before: left-pad infamously broke npm in 2016)
// After: native
'hello'.padStart(10, ' ')

When Dependencies Are Worth It

Not every dependency should be avoided:

# Worth adding despite costs:

React (25M/wk, 45KB):
  Cost: 45KB bundle, 2M+ transitive users' shared dep
  Benefit: JSX, component model, 10-year ecosystem
  Verdict: Core framework — worth it

Zod (12M/wk, 12KB):
  Cost: 12KB bundle, 0 transitive deps
  Benefit: TypeScript-safe validation, type inference
  Verdict: Excellent ROI

Prisma (5M/wk):
  Cost: ~500KB generated client, ~15 transitive deps
  Benefit: Full ORM with migrations, type safety, Studio
  Verdict: Worth it if you're using a database

lodash.groupby (cherry-picked, 2KB):
  Cost: 2KB
  Benefit: Well-tested, edge cases handled
  Verdict: Fine

lodash (full, 70KB):
  Cost: 70KB, unnecessary for single functions
  Benefit: Same as cherry-picking
  Verdict: Use cherry-picking instead

Check dependency impact and health scores on PkgPulse.

See the live comparison

View react vs. vue on PkgPulse →

Comments

Stay Updated

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