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.
"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?
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` binaryRequires Node.js 20+. Zero runtime dependencies.
# 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.jsonGenerate a webpack stats.json with:
webpack --profile --json > stats.json| 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. |
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
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 singlenode_modulespackage 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.
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
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_modulesis attributed to a package (the@scope/nameis 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.
import { analyze, run, evaluateBudget } from "bundle-budget-report";
const result = run({ stats: "stats.json", basis: "raw", budget });
// result.analysis, result.findings, result.okExports include analyzeDir, analyzeStats, loadBudget, evaluateBudget,
renderText, renderMarkdown, renderJson, and the size helpers.
Deep dives on bundle optimization from frontend-performance.com:
- JavaScript bundle optimization & code splitting — the overview.
- Webpack bundle analysis techniques — reading a stats file and finding the weight.
- Tree-shaking & dead-code elimination — shrinking heavy third-party packages.
- Dynamic imports & route-based splitting — acting on split suggestions.
MIT © frontend-performance