Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

Shrinking Published Package Size with sideEffects and files

Your published npm tarball is far larger than the code consumers actually use, and bundlers cannot tree-shake it. This page shows how the files allowlist and sideEffects flag cut tarball size and unlock dead-code elimination for consumers.

Exact symptoms and error messages

npm pack --dry-run reports a tarball with source, tests, and configs, and a consumer's bundle analyzer shows your whole library included even when they import one function:

Exact symptoms and error messages npm pack --dry-run reports a tarball with source, tests, and configs, and a consumer's bundle analyzer shows your whole Exact symptoms and error messages npm pack --dry-run reports a tarball with source, tests, and configs, and a consumer's bundle analyzer shows your whole library included even when they import o
Exact symptoms and error messages — the core idea of this section at a glance.
npm notice === Tarball Contents ===
npm notice 40.1kB src/index.ts
npm notice 12.3kB test/index.test.ts
npm notice 2.1kB  tsconfig.json
npm notice 88.4kB dist/index.mjs
npm notice === Tarball Details ===
npm notice package size: 210 kB

Root cause analysis

Two separate problems inflate consumers. First, without a files allowlist npm ships everything not in .npmignore, so source and tests leak into the tarball. Second, without sideEffects: false a bundler must assume importing any module might have observable side effects and therefore cannot drop unused exports. Both are manifest-level contracts, covered in depth in Understanding package.json Fields.

Root cause analysis Two separate problems inflate consumers. Root cause analysis Two separate problems inflate consumers.
Root cause analysis — the core idea of this section at a glance.

The two levers work on different consumers. The files allowlist controls the tarball — what every npm install downloads — so trimming it helps everyone, including users who never bundle. sideEffects controls what a bundler can eliminate from an application build: it is a promise that importing a module for one export does not trigger observable behavior elsewhere, which lets the bundler drop the unused rest. A library can have a small tarball and still be un-tree-shakeable if sideEffects is missing, so both matter independently.

Format matters too. Tree-shaking works reliably only on ESM output, because static import/export lets the bundler prove which bindings are unused; CommonJS require is dynamic and defeats the analysis. A library that ships only CJS, or whose ESM build re-exports through a barrel file that touches every module, hands the consumer a bundle they cannot shrink no matter how correct the sideEffects flag is.

Resolution and configuration patch

Restrict what publishes and declare that your modules are side-effect-free (or list the few that are not):

Resolution and configuration patch Restrict what publishes and declare that your modules are side-effect-free (or list the few that are not): Resolution and configuration patch Restrict what publishes and declare that your modules are side-effect-free (or list the few that are not):
Resolution and configuration patch — the core idea of this section at a glance.
{
  "files": ["dist"],
  "sideEffects": false,
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

If a module does have side effects (a CSS import, a polyfill), list it explicitly: "sideEffects": ["./dist/polyfill.js", "*.css"].

Audit what actually ships and how much of it a consumer can drop. npm pack --dry-run shows the tarball, and a quick import probe through a bundler shows the tree-shaken cost:

# What downloads on install
npm pack --dry-run
# Approximate the tree-shaken cost of a single import
echo "export { one } from 'your-lib';" > probe.mjs
npx esbuild probe.mjs --bundle --minify --format=esm | wc -c

If importing one function still pulls in the whole library, a side-effectful module or a CJS-only build is defeating elimination — fix the format or the sideEffects list, not just the tarball.

CLI validation and debug commands

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# See exactly what will ship
npm pack --dry-run
# Confirm only dist is included
tar -tf $(npm pack 2>/dev/null) | sed 's|package/||'
# Verify a consumer can tree-shake one import
echo "import { one } from 'your-lib';" > probe.ts

Prevention and CI guardrails

  • Always ship a files allowlist rather than relying on .npmignore denylists.
  • Keep sideEffects: false accurate — a false declaration silently breaks consumers who rely on an import's effect.
  • Add a CI check on packed tarball size to catch regressions.
  • Run npm pack --dry-run in review so tarball contents are visible in the PR.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.

Barrel files and the tree-shaking trap

A single index.ts that re-exports every module — a barrel file — is convenient but is one of the most common reasons a sideEffects: false library still bundles whole. If any re-exported module runs code at import time, or if the bundler cannot prove otherwise, importing one symbol through the barrel can drag in siblings.

Barrel vs subpath An impure barrel bundling everything versus direct subpath imports. Impure barrel • index re-exports all • one import drags siblings • whole lib bundled Subpath exports • your-lib/feature • import one slice • minimal footprint
A pure barrel or subpath exports let consumers import one feature without the rest.

Keep barrels pure re-exports with no top-level side effects, and prefer named exports over export * where a module might have initialization code. For libraries with many independent entry points, expose subpath exports (your-lib/feature) so consumers can import a slice directly and skip the barrel entirely — the surest way to guarantee a small footprint regardless of how aggressive the consumer's bundler is.

Measuring size as a CI budget

Size regressions creep in one dependency at a time, so make the packed size a tracked number rather than something you notice when a user complains. A CI step that measures the tarball and the tree-shaken cost of a representative import turns a silent regression into a failing check.

Size budget gate Measure tarball and import cost, fail on regression. measure tarball npm pack size probe import cost tree-shaken bytes fail over budget block regression
A size budget in CI catches a regression on the PR that caused it.
- run: |
    SIZE=$(npm pack --dry-run --json | jq '.[0].size')
    echo "tarball: $SIZE bytes"
    test "$SIZE" -lt 60000   # fail if the tarball crosses the budget

Pair the tarball budget with an import-cost probe so both dimensions are covered: the download every consumer pays, and the bytes a consumer's bundle keeps after tree-shaking. When either crosses its threshold, the PR that introduced it is where the conversation happens — not a support ticket months later.

Frequently Asked Questions

Does sideEffects: false change my library's runtime behavior?

It does not change your code — it is a promise to bundlers that importing a module without using its exports can be safely dropped. If that promise is false (e.g. a module registers a global), list the exception instead.

files vs .npmignore — which should I use?

Prefer files. An allowlist fails safe: anything you forget is simply not published. A denylist fails open, so a new file you forget to ignore ships by accident.

Why can't a consumer tree-shake my CommonJS build?

Tree-shaking relies on static ESM import/export to prove which bindings are unused. CommonJS require is dynamic, so bundlers keep the whole module to be safe. Ship an ESM build (alongside CJS) so consumers can eliminate dead code.

Does a barrel file hurt bundle size?

It can. If the barrel re-exports modules with import-time side effects, importing one symbol can pull in the rest. Keep barrels as pure re-exports, or expose subpath exports so consumers import a feature directly.

Related

Bundling and Build Tooling for Libraries