GitHub Packages · MIT · Node ≥ 18

bundle-pulse

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. 1 Point the @mrlouiswright scope at GitHub Packages in .npmrc

    @mrlouiswright:registry=https://npm.pkg.github.com
  2. 2 Install

    npm i -D @mrlouiswright/bundle-pulse
Installing from GitHub Packages needs a token

Published 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.

~/project — zsh
$ 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]
Sample output from the bundle-pulse README.

Why it exists

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.

Features

Three sizes

raw, gzip (level 9) and brotli (quality 11) for every .js, .mjs, .cjs and .css file; source maps are skipped.

Budgets

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.

Base diff

--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.

Hygiene

unminified files, identical files shipped under two names, budgets that match nothing (renamed entries), and large files with no budget.

Works with any bundler

Vite, webpack, Rollup, esbuild, Parcel, Next.js static output, or plain files.

Usage

# 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 markdown

CI / GitHub Action

permissions:
  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 }

Options & flags

FlagDescription
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-optionalDo 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-rulesPrint rules and default severities
--no-fixesHide fix suggestions in text output
--no-colorDisable colours

Full reference, config file format and API in the README.

Support this project

Support open-source security tools

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

Portrait of Louis Wright

About the author

Louis Wright

Cyber security researcher and software engineer based in the UK.

More tools by Louis Wright

58 tools

Sentinels security scanners & CI gates · 20