Skip to main content

Unpacked Size Trends: Are npm Packages Getting Bigger?

·PkgPulse Team

TL;DR

npm packages are getting bigger on disk but smaller in browser bundles. The unpacked size of top packages has grown ~15% over 5 years — mostly from TypeScript declaration files (.d.ts) and source maps added for better debugging. But gzipped bundle size for the same packages has stayed flat or shrunk, thanks to better tree-shaking and build tools. The metric that matters for users: gzipped bundle, not unpacked npm tarball.

Key Takeaways

  • Unpacked size grew ~15% over 5 years — but mostly from .d.ts and sourcemaps
  • Gzipped browser bundle is flat or shrinking — what users actually download
  • TypeScript declarations can double the unpacked size without affecting bundle
  • Tree shaking means installed size ≠ bundled size for modern packages
  • Outliers: Moment.js (shrinking slowly), Tailwind v4 (smaller), Prisma (growing from features)

Understanding the Size Metrics

# Three different "sizes" you'll encounter:

# 1. Unpacked size (npm tarball extracted)
npm view package-name --json | jq '."dist.unpackedSize"'
# What you see in node_modules/
# Includes: JS files, .d.ts types, source maps, README, LICENSE

# 2. Bundled size (what your build includes)
npx bundlephobia-cli package-name
# Shows: minified + gzipped
# After tree-shaking: much smaller than unpacked

# 3. Tree-shaken size (what users download)
# Depends on your import patterns:
import { format } from 'date-fns';           // ~1KB for just format()
import * as dateFns from 'date-fns';         // ~80KB for entire library

# The right metric:
# → Evaluating supply chain: unpacked size (what's on your server)
# → Performance impact: gzipped bundle size
# → Security exposure: dependency count (not size)

Size Growth Breakdown

What's making packages "bigger" on disk (unpacked):

1. TypeScript declaration files (.d.ts): +20-40% typical
   → React @types: each major version adds more precise types
   → Package ships types in /dist/*.d.ts
   → Not bundled by any tool — just type-checking artifacts

2. Source maps: +30-50% of JS size
   → .map files: enable debugging the original source in browser DevTools
   → Not bundled unless you explicitly configure it
   → Production sourcemaps: separate upload to Sentry/DataDog

3. CJS + ESM dual packages: 2x size
   → Modern packages ship both CommonJS (/dist/cjs/) and ESM (/dist/esm/)
   → You use one, the other sits there
   → Node tools slowly eliminating the need for CJS

4. Multiple build targets:
   → react: separate browser, server, profiling builds
   → Each needed for different environments

What's NOT making packages bigger:
→ Better tree-shaking actually removes unused code
→ Gzip compression catches repetitive code patterns
→ Smaller algorithmic choices (some libraries rewrote for smaller output)

Packages That Shrunk

# Tailwind CSS v4 — significant size reduction
# v3: 6.2MB unpacked (large CSS processing engine)
# v4: 1.9MB unpacked (rewrote in Rust, moved processing to build time)
# Browser bundle: near-zero runtime JS in v4

# Zod v4 (upcoming) — expected size reduction
# v3: 14KB gzipped
# v4 preview: ~6KB gzipped (rewrote internals for performance)

# date-fns v3
# v2: 81KB gzipped full import
# v3: Better tree-shaking, per-function imports documented clearly
# Average project usage: ~3-8KB (only imported functions)

# React 19
# Bundle size: slightly smaller than React 18
# Eliminated some compatibility code for old React patterns
# New hooks added but net size about the same

# esbuild: consistently tiny
# 0.19: 8.5MB unpacked (Go binary included)
# 0.21: 9.1MB (multiple architectures)
# The binary is large; the API package is tiny

Packages That Grew (For Good Reasons)

# Next.js
# next@12: ~75MB unpacked
# next@15: ~145MB unpacked (+93%)
# Why: Added App Router code, Edge runtime support, Turbopack integration
#      More sophisticated build tooling, more bundled utilities
# Browser bundle impact: minimal — Next.js code doesn't ship to browser

# Prisma
# @prisma/client@4: ~2MB unpacked
# @prisma/client@5: ~4MB unpacked (+100%)
# Why: More database adapters, edge runtime support, TypeScript improvements
# Query engine: separate binary, ~30MB additional but downloaded separately

# TypeScript
# typescript@4: ~60MB unpacked
# typescript@5: ~85MB unpacked (+40%)
# Why: More built-in library definitions, better LSP support files
# None of this goes to production

# @types/node
# Growing with each Node.js version
# More built-in modules = more type declarations
# Only used in development, never bundled

Checking Size Before Installing

# bundlephobia: web UI
# bundlephobia.com/package/package-name

# CLI equivalent:
npx bundlephobia-cli react          # 6.4kB gzipped
npx bundlephobia-cli moment         # 72.1kB gzipped — avoid
npx bundlephobia-cli date-fns       # 86.3kB (full), but tree-shakeable
npx bundlephobia-cli dayjs          # 2.7kB gzipped

# Check multiple alternatives at once:
npx bundlephobia-cli zustand jotai valtio
# Package           Size
# zustand           1.8kB
# jotai             3.1kB
# valtio            2.5kB

# Check tree-shakeability:
npx bundlephobia-cli date-fns --tree-shaking
# Shows: has-side-effects flag, whether tree-shaking works

# Package's sideEffects field in package.json:
npm view date-fns --json | jq '.sideEffects'
# false → fully tree-shakeable
# true or absent → may not tree-shake cleanly

The Dual Package Pattern (CJS + ESM)

# Modern packages ship both CJS and ESM in the same tarball
# Doubles the unpacked size, halves the user's compatibility issues

# package.json pattern:
{
  "main": "./dist/index.cjs",       # CommonJS (Node.js require())
  "module": "./dist/index.mjs",     # ESM (import)
  "exports": {
    "require": "./dist/index.cjs",
    "import": "./dist/index.mjs",
    "types": "./dist/index.d.ts"
  }
}

# Your bundler picks the right one:
# Vite: uses .mjs automatically (tree-shakeable)
# Next.js: uses .mjs for client, .cjs for server
# Node.js 18+: uses .mjs with type: "module"

# The future: ESM-only packages are emerging
# Node.js 22+: all package types supported without workarounds
# Eventually CJS versions won't be needed → packages will shrink

Size Optimization Checklist

# Reduce your bundle size:

# 1. Check your current bundle
npm install -D @next/bundle-analyzer  # Next.js
# Or: npx vite-bundle-visualizer      # Vite
# Shows: which packages are largest in YOUR bundle

# 2. Find heavy packages
npx cost-of-modules --no-install
# Shows: size contribution of each dep in node_modules

# 3. Replace obvious offenders
# moment (72KB) → dayjs (2.7KB)
# lodash (71KB full) → individual functions or built-ins
# uuid (14KB) → nanoid (1KB) or crypto.randomUUID()
# axios (11KB) → ky (2.5KB) or native fetch

# 4. Verify tree-shaking works
# Import specifically, not the whole package:
import { format, parseISO } from 'date-fns';  # Good: only ~2KB
import * as dateFns from 'date-fns';          # Bad: ~80KB

# 5. Check vendor chunks
# In Next.js: vendor bundle > 200KB = investigate
# In Vite: use import() for lazy loading large components

# 6. Image optimization (often bigger win than package size)
# next/image, Astro's built-in optimizer, or sharp
# An unoptimized hero image can be 10x larger than your entire JS bundle

Compare bundle sizes and package health at PkgPulse.

Comments

Stay Updated

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