Skip to main content

How to Reduce Your node_modules Size by 50%

·PkgPulse Team

TL;DR

Most projects can cut node_modules by 30-60% without removing a single dependency. The gains come from: deduplication (multiple versions of the same package), separating devDependencies from production installs, replacing heavy packages with lighter alternatives, and using pnpm's content-addressable storage across projects. A 500MB node_modules often has 150MB of duplicate lodash and old library versions hiding inside.

Key Takeaways

  • Deduplication: npm dedupe removes duplicate packages within a version range
  • Production installs: npm ci --omit=dev skips devDependencies in production
  • Heavy replacements: moment.js (72KB) → date-fns (3KB typical), lodash → native
  • pnpm: stores packages once globally — 60% less disk across projects
  • depcheck: finds installed packages you don't actually import

Step 1: Audit What You Have

# See total node_modules size
du -sh node_modules/
# Common outputs: 300MB - 2GB

# Find the largest packages
du -sh node_modules/* | sort -rh | head -20
# Typical large offenders:
# node_modules/webpack      ~100MB (devDependency, shouldn't be in production)
# node_modules/typescript   ~60MB  (devDependency)
# node_modules/moment       ~72MB  (should be date-fns)
# node_modules/@aws-sdk     ~200MB (tree-shake it)

# Find duplicate packages
npm ls | grep UNMET
npm ls react | grep -v deduped  # See if multiple React versions exist

# Or use: npm-check-duplicates
npx npm-check-duplicates

Step 2: Deduplicate

# npm: deduplicate hoisted packages
npm dedupe
# Finds packages that satisfy multiple version ranges with one version
# Example: pkg A needs lodash ^4.17.0, pkg B needs lodash ^4.15.0
# Before dedupe: 2 copies of lodash
# After dedupe: 1 copy (4.17.21 satisfies both)

# Check what changed:
git diff package-lock.json | grep '"resolved"' | wc -l

# pnpm: deduplication is the default
# pnpm uses a content-addressable store — no duplicates possible
pnpm dedupe  # Explicit dedup for lockfile optimization

# yarn:
yarn dedupe
# Removes duplicate packages in .yarn/cache

Step 3: Remove Unused Packages

# depcheck — finds packages you import vs what's in package.json
npx depcheck

# Output example:
# Unused dependencies:
#   * lodash (imported nowhere but package.json has it)
#   * @types/express (but you have types in package — devDep only)
#
# Missing dependencies:
#   * date-fns (imported but not in package.json — transitive dep you're relying on)

# Remove truly unused:
npm uninstall lodash
npm uninstall @unused-package/thing

# Also check: packages used only in one file that could use a native API
# Example: using `uuid` only for `uuid.v4()` → replace with `crypto.randomUUID()`

Step 4: Replace Heavy Packages

# Biggest wins by package replacement:

# moment.js (72KB) → date-fns (~3KB typical usage)
npm uninstall moment
npm install date-fns
# Savings: ~70KB bundle, significant node_modules reduction

# lodash (70KB) → native JavaScript
npm uninstall lodash
# Many lodash functions have native equivalents:
# _.map → Array.map
# _.filter → Array.filter
# _.reduce → Array.reduce
# _.find → Array.find
# _.get → optional chaining (?.)
# _.merge → Object.assign / spread
# _.debounce → keep (or use usehooks-ts/useDebounce)
# If you only need 5 functions, use lodash-es/treeShake or individual packages

# request (deprecated) → ky or native fetch
npm uninstall request
npm install ky  # 3KB, modern, Promise-based

# chalk → colorette or picocolors (for CLI tools only)
npm uninstall chalk
npm install picocolors  # 0.5KB vs chalk's 6KB

# uuid → native crypto.randomUUID()
npm uninstall uuid
# In code: crypto.randomUUID() works in Node 15+, Chrome 92+, Safari 15+

Step 5: Separate Dev vs Production

# Ensure devDependencies are correctly classified
# DevDeps: TypeScript, testing tools, bundlers, linters
# Deps: runtime libraries your app actually uses

# Check production install size vs full install:
npm ci --omit=dev  # Production: skips devDependencies
du -sh node_modules/
# Should be 50-80% smaller than full install

# Common mismatch — things in "dependencies" that should be "devDependencies":
# - typescript
# - vitest / jest
# - @types/*
# - eslint, biome, prettier
# - webpack, vite, rollup
# - ts-node (use tsx for runtime)

# Fix:
npm install --save-dev typescript  # Moves to devDependencies

Step 6: AWS SDK Optimization

# @aws-sdk v3 is modular — only import what you use
# Before (v2, whole SDK):
npm install aws-sdk  # 200MB+, everything included

# After (v3, modular):
npm install @aws-sdk/client-s3         # Only S3
npm install @aws-sdk/client-dynamodb   # Only DynamoDB
# Each client: ~5-15MB vs 200MB for full SDK

# Tree-shaking in bundled apps:
# Vite and webpack will tree-shake unused SDK exports automatically
# But in Node.js (unbundled), you still need the right package

Step 7: Switch to pnpm for Global Efficiency

# The pnpm advantage: global content-addressable store
# ~/.pnpm-store/v3/ — all packages stored by hash
# Multiple projects share packages — stored ONCE on disk

# Before pnpm (npm):
# project-a/node_modules: 300MB
# project-b/node_modules: 280MB
# project-c/node_modules: 310MB
# Total: 890MB

# After pnpm:
# project-a/node_modules: symlinks → 50MB (unique deps)
# project-b/node_modules: symlinks → 40MB (unique deps)
# project-c/node_modules: symlinks → 45MB (unique deps)
# ~/.pnpm-store: 400MB (shared by all)
# Total: 535MB — 40% savings

# Migration:
rm -rf node_modules package-lock.json
npm install -g pnpm
pnpm install  # Creates pnpm-lock.yaml

Measuring Progress

# Before:
du -sh node_modules/           # 487MB

# Run all steps above

# After:
npm dedupe
npx depcheck  # Remove unused
# Replace heavy packages
npm ci         # Clean reinstall
du -sh node_modules/           # 241MB  ← 50% smaller

# Bundle size impact (separate from node_modules):
npm run build
# Check dist/ size — this is what users download
# node_modules size ≠ bundle size (bundlers tree-shake)

Analyze npm package bundle sizes on PkgPulse.

See the live comparison

View npm vs. pnpm on PkgPulse →

Comments

Stay Updated

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