Skip to main content

How Package Popularity Correlates with Bundle Size 2026

·PkgPulse Team
0

TL;DR

Popular packages are not systematically larger — the correlation between downloads and bundle size is weak. The most downloaded packages span the full size spectrum: lodash (71KB), react (6.4KB), and semver (0.1KB) are all in the top 50 by downloads. What actually predicts size: problem domain complexity, historical API baggage, and whether the package was designed for tree-shaking. The most important insight: new popular packages tend to be smaller than old popular packages.

Key Takeaways

  • No strong correlation between downloads and bundle size
  • Old popular packages are larger (pre-ESM, pre-tree-shaking era)
  • New popular packages are smaller (designed for modern bundlers)
  • The "popular = bloated" perception is because famous packages like Moment.js are old
  • Bundlephobia score is more predictive of whether you should use a package than download count

The Size Spectrum at the Top

Top packages by downloads with their gzipped bundle sizes:

< 5KB (tiny):
→ semver: 0.1KB (95M dependents)
→ ms: 0.5KB (15M/week)
→ clsx: 0.5KB (5M/week)
→ nanoid: 1.1KB (8M/week)
→ dayjs: 2.7KB (8M/week)
→ zustand: 1.8KB (8M/week)
→ react: 6.4KB (50M/week — surprisingly small!)

5-50KB (medium):
→ tailwindcss (runtime): 7.5KB (45M/week)
→ @tanstack/react-query: 13KB (10M/week)
→ zod: 14KB (14M/week)
→ axios: 11KB (35M/week)

50-150KB (large):
→ lodash: 71KB (28M/week) — but tree-shakeable
→ date-fns: 81KB full (14M/week) — very tree-shakeable
→ framer-motion: 47KB (5M/week)

> 150KB (very large):
→ moment: 300KB (14M/week) — avoid for new projects
→ three.js: 600KB (5M/week) — 3D library, expected
→ chart.js: 62KB (5M/week)
→ firebase: 200KB+ (2M/week)

The download and size data reveals an anti-correlation that surprises many developers: some of the highest-download packages in the npm ecosystem are tiny, while some lower-download packages are enormous. The explanation is historical, not technical. semver with 95 million dependents is downloaded constantly because virtually every package management tool depends on it — but solving version range comparison is intrinsically a small problem, requiring roughly 0.1KB of code. React at 50 million weekly downloads is 6.4KB because the component model is architecturally minimal: React's job is reconciliation and event delegation, not DOM manipulation or utility functions. Size is determined by the problem domain, not by importance.

The Moment.js position tells the other side of this story. Moment gained 14 million weekly downloads in an era before bundle size was prominently measured — before bundlephobia existed as a standard reference, before Core Web Vitals tied JavaScript size to SEO rankings. Developers chose Moment because it was comprehensive, well-documented, and the best-known option. Nobody was weighting 300KB against a 2.7KB alternative, because the alternative didn't exist yet and the cost wasn't visible. By the time both conditions changed, Moment was deeply entrenched.

The metric that most reliably predicts which currently popular packages face future download decline is size relative to functionally equivalent alternatives. When a smaller alternative covers the same core use cases, the larger package loses new-project share over time as developers default to the lighter option. The Moment-to-dayjs transition was the template; the Redux-to-Zustand transition followed the same dynamic. Monitoring this trajectory for packages in your dependency tree gives early warning of when a migration becomes worthwhile.


Packages published pre-2018 were designed in a different era:

1. CommonJS (not ESM): no tree-shaking possible
   → You installed lodash → you got ALL of lodash
   → Even if you only used _.map, you paid for _.flatten too
   → Solution: lodash became tree-shakeable in lodash-es

2. Monolithic APIs: all functions in one bundle
   → Moment.js: all locales included (you can tree-shake but it's not obvious)
   → jQuery: entire DOM library even if you need 5%

3. No bundler-aware packaging:
   → "sideEffects: false" didn't exist
   → Packages couldn't tell bundlers "this is tree-shakeable"

4. Feature creep over time:
   → Good package → users request features → size grows
   → v1: 10KB → v5: 50KB (same core problem, 5x code)
   → Moment.js: started small, added locales, plugins, timezone → 300KB

New packages (post-2020) learn from this:
→ Day.js launched knowing it would replace Moment.js: stayed at 2.7KB
→ Zustand launched knowing it would replace Redux: designed for tree-shaking
→ Zod: functional design enables tree-shaking
→ Valibot: went further — explicitly modular, no central bundle

The shift to ES modules and tree-shaking represents one of the most consequential changes in the JavaScript ecosystem over the past decade. Tree-shaking — a bundler's ability to eliminate exports that are never imported — depends on static import/export statements that bundlers can analyze at build time. CommonJS require() is dynamic by nature, which makes complete tree-shaking impossible: a bundler cannot statically determine which exports will be accessed at runtime.

Lodash illustrates the transition problem. The original lodash package ships as CommonJS, meaning importing it gives you the entire 71KB library regardless of which three functions you actually use. lodash-es, the ES module variant, supports full tree-shaking: import { omit } from 'lodash-es' bundles only omit and its internal dependencies — roughly 1KB. Same functionality, same API, dramatically different cost depending on import style.

The sideEffects: false field in package.json is the signal bundlers use to know a package is safe for aggressive tree-shaking. It tells webpack, Rollup, and Vite: "no import from this package triggers side effects when loaded." Packages that omit this field get treated as having potential side effects, meaning bundlers cannot eliminate unused exports even when using ES modules. Checking whether a package sets sideEffects: false is a useful one-line signal before adding it to a production bundle — a 50KB package that tree-shakes correctly might cost 3KB for your actual usage pattern. The same package without the flag costs 50KB regardless.


Packages launched 2020-2026 that became popular while staying small:

Type        Package             Launch  Gzipped  Downloads
Dates:      dayjs               2018    2.7KB    8M/week
State:      zustand             2020    1.8KB    8M/week
IDs:        nanoid              2017    1.1KB    8M/week
Classes:    clsx                2018    0.5KB    5M/week
HTTP:       ky                  2018    2.5KB    2M/week
Toasts:     sonner              2023    3.2KB    800K/week
Router:     wouter              2018    2.4KB    500K/week
Events:     mitt                2016    0.3KB    2M/week
Storage:    unstorage           2022    ~5KB     600K/week

Pattern: Modern popular packages are tiny by design.
Package authors in 2020+ know:
1. bundlephobia.com will show their size prominently
2. Users penalize large bundles
3. Tailwind's success showed "utility-first = small" wins
4. React ecosystem has bundle size culture

Compare to equivalent old packages:
dates:    moment    → 300KB  (vs dayjs 2.7KB)
state:    redux+RM  → 40KB   (vs zustand 1.8KB)
events:   eventemitter3 → 2KB (vs mitt 0.3KB)

The "modern packages are small by design" pattern reflects a design philosophy that has become normative among package authors who care about adoption. When you publish a new npm package, bundlephobia displays your size prominently on the npm page — the number is visible before the install command runs. This visibility created feedback pressure that gradually moved the distribution of new package sizes downward.

The dayjs-versus-moment comparison is the canonical case. dayjs launched in 2018 with an explicit size target, knowing its differentiation story was "moment.js API at 1% of the cost." Every API decision was filtered through "does this keep the bundle under our target?" That discipline, only possible because moment.js existed as a benchmark to contrast against, produced a package that solved the same problem for 95% of users at a fraction of the cost.

The pattern has replicated across categories: Valibot versus Zod (Valibot takes the per-function import approach to get below 2KB for typical validation setups), Nano Stores versus Redux (minimal state primitives rather than a full opinionated system), Mitt versus EventEmitter3 (event emitter at 300 bytes). In each case, the newer package defined its bundle target as a design specification, not a performance afterthought. The culture of bundle size accountability in the React ecosystem — where bundlephobia data circulates naturally in library discussions — made this design discipline a competitive advantage rather than just engineering aesthetics.


Some packages are popular AND large — for good reasons:

three.js (600KB, 5M/week):
→ 3D rendering library — it IS the 3D engine
→ If you need WebGL, you need this
→ Can be tree-shaken to just the modules you use
→ There's no "small" 3D rendering library

framer-motion (47KB, 5M/week):
→ Animation is complex — physics simulation, spring calculations
→ Tree-shakeable: basic animation = 20KB
→ Accepted cost for quality of DX

firebase SDK (200KB+, 2M/week):
→ Includes: auth, firestore, storage, analytics
→ Modular since v9: import only what you use
→ Using just auth: ~30KB

echarts (900KB!, 3M/week):
→ Full-featured data visualization
→ Tree-shakeable but complex
→ For simple charts: Chart.js (62KB) or Recharts (52KB) are better

These are acceptable large bundles because:
→ The functionality justifies the size
→ There's no genuinely smaller alternative with same capabilities
→ They've invested in tree-shaking so partial usage is smaller

The legitimate large packages illuminate an important distinction: size should be proportional to the problem's inherent complexity, not to implementation choices. Three.js at 600KB is serving WebGL — the browser's low-level 3D graphics API — and provides a complete scene graph, renderer, geometry library, material system, and animation framework. There is no way to solve the same problem meaningfully smaller; the complexity is in the domain. This is fundamentally different from Moment.js's 300KB, which reflected historical accumulation rather than domain necessity.

The Firebase SDK's modular v9 restructuring is the case study for how large packages can address size concerns without sacrificing capability. Before v9, importing firebase gave you the entire SDK. After v9, each service (auth, Firestore, storage) is a separately importable module, and tree-shaking reduces the practical bundle cost to the surface area you actually use. Using only Firebase Authentication costs roughly 30KB — a fraction of the 200KB+ figure. This restructuring required a breaking API change and significant engineering effort, but it addressed a real adoption barrier.

The judgment call when evaluating large packages: is the size driven by the domain or by implementation debt? Three.js and Framer Motion are doing inherently complex things — real-time physics, WebGL rendering. A large package that does simple things (configuration parsing, string formatting, simple data manipulation) should be viewed skeptically against smaller alternatives. The question "is there a genuinely smaller alternative with the same core capabilities?" answers this definitively. If no smaller alternative exists, the size is justified. If one does, you're carrying implementation debt.


Checking the Size-to-Value Ratio

# Before installing, calculate size-to-value:

# Step 1: Check bundle size
npx bundlephobia-cli package-name

# Step 2: Ask: what does this give me?
# Is this problem solvable without the package?
# - Is there a built-in Node.js/Web API?
# - Is there a smaller package with same functionality?

# Step 3: Check tree-shakeability
npm view package-name --json | jq '.sideEffects'
# false → tree-shakeable (you pay for what you use)
# true/undefined → might pay for everything

# Step 4: Check what you'd actually use
# bundlephobia shows full size; your actual usage is often smaller

# Example calculation:
# date-fns: 81KB full, but:
# import { format, parseISO, addDays } from 'date-fns';
# Actual bundled: ~3KB (just those 3 functions)
# Worth it? Yes. 3KB for ergonomic date handling.

# vs moment: 72KB — not tree-shakeable in same way
# Worth it for new projects? No. Use dayjs (2.7KB) instead.

# The rule:
# Large non-tree-shakeable packages: justify with "no good alternative"
# Large tree-shakeable packages: calculate actual usage cost
# Small packages: rarely need to justify

The bundlephobia-cli check should become a reflex before any npm install in a production codebase. The data it returns — size in KB, number of dependencies, tree-shakeability status — takes ten seconds to review and gives the complete cost picture before the dependency is entrenched. Teams that build this into their workflow report fewer "how did our bundle get this big" conversations, because the cost is visible at the point of decision rather than discoverable only when a performance regression surfaces.

The "is there a built-in alternative?" question deserves emphasis because the platform has matured enough to eliminate entire dependency categories. uuid is 14KB; crypto.randomUUID() is zero bytes, built into Node 15+ and all modern browsers. axios is 11KB; native fetch with a small error-handling wrapper covers 90% of use cases at zero bytes. path.join from Node.js is zero bytes for server-side code. lodash.merge is replaceable by structuredClone or spread operators for most use cases. These substitutions reduce bundle size, reduce dependency count, and eliminate packages that require security monitoring.

The tree-shakeability flag changes the calculation for large packages fundamentally. date-fns at 81KB sounds expensive until you understand that using format, parseISO, and addDays costs roughly 3KB after tree-shaking — because each function is an independent module. The 81KB is a catalog, not an invoice. Contrast with a 30KB package that sets sideEffects: true: you pay 30KB regardless of which 2KB of functionality you import. Non-tree-shakeable packages always cost their full size; tree-shakeable ones charge only for what you use.


The Bundle Budget Approach

// Set a budget for your JavaScript bundle:
// "First meaningful paint" target: < 200KB JS total

// Track your budget in CI:
// next.config.ts
import { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    // Next.js built-in size monitoring
  },
};

// Or: vite-plugin-analyze
// vite.config.ts
import analyze from 'rollup-plugin-analyzer';
export default defineConfig({
  plugins: [
    ...(process.env.ANALYZE ? [analyze({ summaryOnly: true })] : []),
  ],
});
// Run: ANALYZE=true vite build

// Budget targets (approximate):
// Content sites: < 50KB JS
// Marketing sites: < 100KB JS
// SaaS applications: < 300KB JS (first load)
// Complex dashboards: 500KB JS (acceptable for authenticated apps)

// When you exceed your budget:
// 1. Find the biggest chunks (bundle analyzer)
// 2. Check for duplicate packages (two versions of same lib)
// 3. Lazy-load heavy components: import('./HeavyChart')
// 4. Replace large packages with smaller alternatives

Bundle budget targets are anchored in device and network reality, not arbitrary preferences. The 50KB JavaScript recommendation for content sites tracks measured user behavior: time-to-interactive above roughly two seconds on median mobile hardware (4G connection, mid-tier Android device) produces measurable bounce rate increases. A strict 50KB JavaScript budget keeps time-to-interactive reliably within that window for most users on most real-world network connections.

Authenticated SaaS applications get more latitude because the user has already committed to the product — the decision to use it precedes the load. The 300KB first-load target for SaaS corresponds to roughly 2-3 seconds on mid-tier mobile hardware, within the window where invested users wait. After first load, aggressive caching means returning users pay far less; the budget applies primarily to the initial visit.

The budget approach reframes package decisions as portfolio decisions. When a team has agreed on a 200KB budget and sits at 170KB, adding a 40KB package requires either removing 10KB elsewhere or formally raising the budget with stakeholder visibility. This constraint makes tradeoffs explicit at the moment they're made. Without a budget, each package decision is evaluated in isolation and cumulative cost becomes invisible until a performance regression surfaces months later — typically after the dependencies responsible are already entrenched.


The relationship between popularity and bundle size has inverted over the past eight years. The old pattern (circa 2018): the most popular packages were often among the largest. jQuery peaked at 30KB when size wasn't a primary developer concern. Moment.js hit 72KB with all locales and became the default choice for date handling despite the cost. Lodash at 70KB was the utility library every project included in full. These packages became popular before bundle size was routinely measured or before tooling surfaced that measurement prominently.

The new pattern (2026): the packages growing fastest in download velocity trend small. Vite's rise corresponds directly to fast build performance, which is inseparable from its architecture choice to use esbuild. Zustand's growth tracks its 1.8KB footprint — Redux provided the same state management capability at roughly 40KB combined with React-Redux. Nanoid's adoption accelerated as uuid (14KB) became the obvious overweight alternative for ID generation, a problem that turns out to require about 1KB to solve well.

The underlying cause is a structural change in how developers evaluate packages. Core Web Vitals added financial incentive — SEO ranking and conversion rates tied directly to JavaScript payload size — that upstream developer tooling decisions now visibly affect. Bundlephobia's visibility in the npm ecosystem surfaced size information at the point of decision. The npm package page links to bundlephobia; the size is visible before the install command runs.

The download velocity signal has become a useful leading indicator: when a package's download velocity is accelerating beyond 10% month-over-month in a category that already has established incumbents, check whether bundle size is part of the value proposition. In 2026, "small and focused" is a genuine marketing advantage in the npm ecosystem, not just a technical preference.

The inverse is also true: packages that are losing download share in competitive categories are often losing it to smaller alternatives. When you observe a formerly dominant package declining in relative download growth while retaining its absolute count, it typically means new projects are selecting alternatives while existing projects haven't migrated yet. That pattern signals a migration opportunity — the ecosystem is already moving, and you can get ahead of it by evaluating the successor package before the migration becomes urgent. Lodash's trajectory (absolute downloads stable, share declining as projects use native ES methods) illustrates this pattern cleanly. The downloads are maintained by existing codebases; new codebases increasingly skip lodash entirely.

The relationship between popularity and bundle size in 2026 is best summarized as: popularity doesn't predict size, but trajectory does. New packages gaining share are consistently smaller than old packages losing share in the same category. The long-run direction is toward smaller default choices, driven by measurement infrastructure that makes bundle cost visible at the point of decision and by a generation of package authors who experienced the Moment.js story firsthand and deliberately designed against it.


Reading the Size-to-Value Ratio for Better Decisions

Before adding any package, calculating value density — functionality per kilobyte — clarifies whether the cost is justified. A library that provides date formatting, timezone handling, relative time display, calendar arithmetic, and locale support for 23KB (date-fns, tree-shaken to what you actually use) has high value density. A library that adds one CSS class naming utility for 8KB has low value density because the problem is solvable with a five-line function.

The practical tool is bundlephobia.com, which surfaces three pieces of information that together tell the full story: bundle size (what you pay), whether the package has side effects (whether tree-shaking can reduce that cost), and tree-shakeability (whether partial imports work). A non-tree-shakeable package at 30KB costs 30KB regardless of how little you use. A tree-shakeable package at 81KB (date-fns) might cost 3KB for the three functions you actually need.

The comparison heuristic before installing any package: run npx bundlephobia-cli <package-name> to see size and tree-shakeability, then ask what stdlib or smaller alternative achieves 80% of the functionality at 0KB. This mental model surfaces the "good enough" native alternatives — Array.from, Object.fromEntries, structuredClone, native fetch — that eliminate the dependency entirely for many common use cases.

The packages with the best size-to-value ratios in 2026 are worth knowing as reference points: Zod provides comprehensive schema validation and type inference at 14KB. Zustand delivers complete state management including devtools integration at 1.8KB. Valibot takes the modular approach further — granular per-function imports result in roughly 2KB for a typical validation setup. clsx handles the className merging problem that every React project encounters at 0.5KB. These packages demonstrate what disciplined API design produces: complete solutions to real problems at costs that are easy to justify.

The high value density of these packages also makes them significantly easier to defend in code review when someone questions whether a new dependency is worth adding. "We are adding 1.8KB for a complete reactive state management solution" is a different conversation from "we are adding 40KB for a state management library." When the cost is proportionate to the value, the dependency decision is straightforward. When the cost is disproportionate, the conversation becomes about whether a lighter, more focused alternative exists — and in 2026, for most common frontend problems, a well-maintained, well-documented one usually does exist.


Compare bundle sizes and popularity data for npm packages at PkgPulse.

See also: Bun vs Vite and AVA vs Jest, The Smallest Bundle: Top npm Packages Under 5KB.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.