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.
See the live comparison
View vite vs. webpack on PkgPulse →