Three sizes
raw, gzip (level 9) and brotli (quality 11) for every .js, .mjs, .cjs and .css file; source maps are skipped.
GitHub Packages · MIT · Node ≥ 18
A CI gate for JavaScript and CSS bundle size: budgets on raw, gzip and brotli sizes, and a diff against your base branch.
CI gate for JS/CSS bundle size: per-entry budgets on raw, gzip and brotli sizes, total JS/CSS budgets, growth vs a base-branch snapshot with content-hash-insensitive names, unminified and duplicate files. SARIF, Markdown, GitHub Action. Zero deps.
1 Point the @mrlouiswright scope at GitHub Packages in .npmrc
@mrlouiswright:registry=https://npm.pkg.github.com2 Install
npm i -D @mrlouiswright/bundle-pulsePublished to GitHub Packages, not npmjs.com. GitHub Packages needs a GitHub token with read:packages to install, even for public packages: add //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} to .npmrc or run npm login --registry=https://npm.pkg.github.com.
$ npx bundle-pulse test/fixtures/badbundle-pulse — test/fixtures/bad5 file(s): JS 32.6 kB gzip, CSS 10.9 kB gzip HIGH Total JS is 32.6 kB gzip, over the 30.0 kB budget [bundle-total-over-budget]HIGH test/fixtures/bad/assets/app-K2m9xQ7r.css assets/app-K2m9xQ7r.css is 10.9 kB gzip, over its 6.0 kB budget by 4.9 kB (82%) [bundle-over-budget]HIGH test/fixtures/bad/assets/index-Xa91bQ2k.js assets/index-Xa91bQ2k.js is 9.1 kB gzip, over its 8.0 kB budget by 1.1 kB (14%) [bundle-over-budget]MEDIUM Budget "assets/admin-*.js" matches no file [bundle-budget-unmatched]MEDIUM test/fixtures/bad/assets/legacy.js assets/legacy.js (40.0 kB) looks unminified [bundle-unminified]
Bundles grow one innocent import at a time. bundle-pulse measures every JS and CSS file in your build output the way browsers receive it (gzip and brotli as well as raw), checks each file against the budget for its glob, checks total JS and CSS, and compares against a snapshot from the base branch so a pull request that adds 40 kB to the main chunk fails with a clear message. Content hashes in file names (index-BdT9x2Qa.js, main.3f2a9c1b.chunk.js) are stripped when comparing, so the diff follows the entry, not the file name.
CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies, runs offline. By Louis Wright.
raw, gzip (level 9) and brotli (quality 11) for every .js, .mjs, .cjs and .css file; source maps are skipped.
per-glob budgets on any of the three sizes, total JS and total CSS budgets, and a warning when a file passes 90% of its budget.
--snapshot writes sizes to JSON; --base compares against it and reports files that grew more than --max-growth percent (default 10%, ignoring changes under 1 kB) and new large chunks.
unminified files, identical files shipped under two names, budgets that match nothing (renamed entries), and large files with no budget.
Vite, webpack, Rollup, esbuild, Parcel, Next.js static output, or plain files.
# Example
npx bundle-pulse dist# Example
npx bundle-pulse dist -b 'assets/index-*.js=60KB' -b 'assets/*.css=brotli:15KB' --total-js 200KB# on main
npx bundle-pulse dist --snapshot bundle-base.json# Example
npx bundle-pulse dist --base bundle-base.json --max-growth 5 --format markdownpermissions:
contents: read
security-events: write # only needed for the SARIF upload
jobs:
bundle-pulse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { persist-credentials: false }
- uses: MrLouisWright/bundle-pulse@v1
with:
path: dist
fail-on: high
sarif: bundle-pulse.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: bundle-pulse.sarif }
| Flag | Description |
|---|---|
| Budgets | |
-b, --budget <glob=size> | Budget for matching files, repeatable; gzip by default ("assets/index-*.js=60KB", |
--total-js <size> | Budget for all JS together (gzip) |
--total-css <size> | Budget for all CSS together (gzip) |
--include <globs> | Files to measure (default **/*.{js,mjs,cjs,css}; source maps always skipped) |
--exclude <globs> | Files to skip |
| Base comparison | |
--base <file> | Snapshot from the base branch (written with --snapshot) |
--base-optional | Do not fail if the base snapshot does not exist yet |
--max-growth <pct> | Allowed growth per file vs base, in percent (default 10) |
--snapshot <file> | Write the current sizes as a snapshot |
| Common | |
-c, --config <file> | JSON config (default: .bundle-pulse.json or "bundlePulse" in package.json) |
-f, --format <fmt> | text (default) | json | sarif | markdown |
--json <file> | Also write a JSON report |
--sarif <file> | Also write SARIF 2.1.0 |
--markdown <file> | Also write Markdown (e.g. for a job summary) |
--fail-on <sev> | critical | high | medium | low | info | none (default: high) |
--list-rules | Print rules and default severities |
--no-fixes | Hide fix suggestions in text output |
--no-color | Disable colours |
Full reference, config file format and API in the README.
Support this project
I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.
Buy me a coffee (opens Stripe in a new tab)Secure checkout by Stripe