Skip to content

About

CLI that reports per-route JS/CSS bundle weight against budgets, flags heavy third-party packages, and suggests code-splitting boundaries — from a built dir or a webpack stats.json. Guides on frontend-performance.com.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

bundle-budget-report

A browserless CLI that reports JavaScript/CSS bundle weight against budgets, flags heavy third-party dependencies, and suggests code-splitting boundaries — straight from build output you already have. Each finding links to a deep-dive on frontend-performance.com.

No bundler, no browser, no network. It reads a built directory or a webpack stats.json, computes real gzip/brotli transfer sizes with Node's built-in zlib, and exits non-zero when you blow a budget — so it drops straight into CI.

Why

"The bundle got big again" is usually noticed in production, not in review. A performance budget turns bundle weight into a build gate: set a ceiling, and the build fails the moment an import pushes an initial route over it. This tool gives you that gate plus the two questions you ask next — which dependency got heavy? and what should I split out?

Install

Not published to npm. Run it directly from GitHub with npx:

npx github:frontend-performance/bundle-budget-report --dir dist/

Or clone and link locally:

git clone https://github.com/frontend-performance/bundle-budget-report.git
cd bundle-budget-report
npm install        # builds via the prepare script
npm link           # exposes the `bundle-budget-report` binary

Requires Node.js 20+. Zero runtime dependencies.

Usage

# Scan a built-output directory (raw + gzip + brotli sizes)
bundle-budget-report --dir dist/

# Parse a webpack stats.json (per-route weight, third-party, split hints)
bundle-budget-report --stats stats.json

# Auto-detect: a directory or a *.json stats file
bundle-budget-report dist/
bundle-budget-report stats.json

# Gate a build against a budget file
bundle-budget-report --dir dist/ --budget bundle-budget.json

# CI-friendly outputs
bundle-budget-report --stats stats.json --markdown >> "$GITHUB_STEP_SUMMARY"
bundle-budget-report --dir dist/ --json > report.json

Generate a webpack stats.json with:

webpack --profile --json > stats.json

Options

Flag Description
--dir <path> Scan a built-output directory for .js / .mjs / .cjs / .css.
--stats <file> Parse a webpack stats.json.
<path> Positional path; auto-detected as a directory or a stats file.
--budget <file> Budget JSON. Defaults to bundle-budget.json if present.
--basis raw|gzip|brotli Size basis for totals and budgets. Default gzip.
--fail-on error|warn|never Severity that makes the process exit non-zero. Default error.
--json Machine-readable JSON.
--markdown Markdown tables for $GITHUB_STEP_SUMMARY / PR comments.
--no-color Disable ANSI colors (also honors NO_COLOR).
-h, --help Show help.

Exit codes

  • 0 — within budget (or nothing to fail on)
  • 1 — a finding at or above --fail-on (default: budget errors)
  • 2 — usage error, or a missing/invalid path or budget file

Budget file

JSON, default name bundle-budget.json. Every key is optional — absent keys are skipped. Sizes are in the active --basis (gzip by default), except thirdParty.maxPackageBytes, which is always raw bytes.

{
  "total":      { "js": 200000, "css": 50000 },
  "entry":      { "main": { "js": 170000 } },
  "thirdParty": { "maxPackageBytes": 40000 }
}
  • total — ceilings for combined JS and CSS across all assets.
  • entry — per-route ceilings, keyed by entry/route name. In stats mode these are real webpack entrypoints; in directory mode they are chunks detected as initial by filename (main, index, app, [slug], …).
  • thirdParty.maxPackageBytes — any single node_modules package heavier than this raises a warning.

Over a size budget is an error (exit 1). Heavy packages are warnings; code-splitting suggestions are info. Use --fail-on warn for a stricter gate.

Sample output

bundle-budget-report  stats.json
mode: stats  ·  basis: raw  ·  4 asset(s)

Entries (initial weight)
  Entry        JS       CSS     Total
  main    146.5 kB   29.3 kB  175.8 kB
  admin    87.9 kB      0 B    87.9 kB

Totals  JS 283.2 kB  ·  CSS 29.3 kB  ·  All 312.5 kB  (raw)

Heaviest third-party packages (raw)
  react-dom                    117.2 kB  1 module(s)
  lodash                        70.3 kB  1 module(s)
  moment                        58.6 kB  1 module(s)
  @scope/ui                     43.9 kB  1 module(s)
    → https://frontend-performance.com/.../tree-shaking-and-dead-code-elimination/

Findings  1 error · 4 warn · 4 info
✗ entry-js:main  Entry "main" JS is 146.5 kB — over the 117.2 kB budget by 29.3 kB (+25%)
    → https://frontend-performance.com/.../dynamic-imports-and-route-based-splitting/
! third-party-heavy  Third-party package "react-dom" is 117.2 kB (raw), over the 40.0 kB limit
i split-candidate  react-dom (117.2 kB raw) sits on initial "main"
...

FAIL
Report generated by bundle-budget-report — https://frontend-performance.com

How it works

Directory mode walks the output directory, reads each .js/.mjs/.cjs/.css asset, and computes three sizes: raw bytes, gzipSync (level 9) and brotliCompressSync (quality 11) — the actual bytes a client downloads. Assets are grouped by type for totals, and initial/entry chunks are detected heuristically from filenames (hashes and cosmetic suffixes like .min are stripped first) so lazy chunks such as chunk-482.abcd.js are not mistaken for entries.

Webpack stats mode reads entrypoints, assets, chunks and modules:

  • Per-route weight — sums each entrypoint's initial JS + CSS assets.
  • Third-party aggregation — every module whose identifier contains node_modules is attributed to a package (the @scope/name is parsed out), and byte sizes are summed per package, including concatenated sub-modules.
  • Split suggestions — a module at or above 30 kB that ships on an initial chunk is flagged as a dynamic import() candidate; the same module on an async chunk is left alone.

Webpack stats only report raw sizes, so in stats mode --basis gzip|brotli falls back to raw (with a note). For true compressed sizes, point the tool at the built directory.

Parsers and analysis are pure functions with no I/O, unit-tested with fixtures.

Programmatic API

import { analyze, run, evaluateBudget } from "bundle-budget-report";

const result = run({ stats: "stats.json", basis: "raw", budget });
// result.analysis, result.findings, result.ok

Exports include analyzeDir, analyzeStats, loadBudget, evaluateBudget, renderText, renderMarkdown, renderJson, and the size helpers.

Further reading

Deep dives on bundle optimization from frontend-performance.com:

License

MIT © frontend-performance

About

CLI that reports per-route JS/CSS bundle weight against budgets, flags heavy third-party packages, and suggests code-splitting boundaries — from a built dir or a webpack stats.json. Guides on frontend-performance.com.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages