Skip to content

Catching a 180KB Bundle Regression Before It Ships

A CI setup using size-limit that fails a PR when bundle size regresses, catching what a manual bundle-analyzer check only finds after the damage ships.

· · 8 min read

Updated: October 6, 2026

Graphs of performance analytics on a laptop screen

Quick Take

`size-limit` runs in CI and fails a pull request when a bundle's compressed size exceeds a configured budget, catching regressions at review time instead of after they ship. Set the budget from your current measured size plus 10-20% headroom, not an arbitrary number, and pick the compression (gzip or brotli) on purpose, because the default changed and the two numbers differ by about 10 percent.

size-limit is a CI-integrated tool that measures a JavaScript bundle's compressed size on every pull request and fails the build when it exceeds a configured numeric budget. The 180KB regression that prompted this setup came from a single dependency version bump, a patch release that pulled in a new transitive dependency neither the PR author nor the reviewer noticed in a diff full of otherwise-unrelated package.json changes. A human review missed it because bundle size isn't something a diff makes visible. A CI check would have.

How Do You Set Up size-limit?

npm install --save-dev size-limit @size-limit/preset-app
// package.json
{
  "size-limit": [
    {
      "name": "Main bundle",
      "path": "dist/assets/index-*.js",
      "limit": "180 KB"
    },
    {
      "name": "Vendor bundle",
      "path": "dist/assets/vendor-*.js",
      "limit": "120 KB"
    }
  ],
  "scripts": {
    "size": "size-limit"
  }
}

Each entry names a specific output chunk and its budget. Bundle budget is a numeric ceiling, expressed in kilobytes, that a specific build output is not allowed to exceed without failing a CI check. Splitting the budget by chunk (main app code versus vendor/dependency code) rather than one combined number makes a regression easier to diagnose from the CI failure alone, "vendor bundle exceeded its limit" points straight at a dependency change, where a single combined budget would just say "something got bigger" without narrowing down where. The preset runs its own bundler and minifier, so the number it reports is minified plus compressed regardless of what your build emitted. I checked this on size-limit 14.1.0 with @size-limit/preset-small-lib, which reports "with all dependencies, minified and brotlied" unless you say otherwise. Compression is a setting, not a given, so set it in the config.

How Do You Wire It Into GitHub Actions?

# .github/workflows/size-check.yml
name: Bundle Size Check

on:
  pull_request:
    branches: [main]

jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run build
      - name: Check bundle size
        run: npx size-limit

npx size-limit exits with a non-zero code if any chunk exceeds its budget, which fails the CI job and blocks the PR from being merged (assuming this check is set as required in the repo's branch protection rules). The failure message names the specific chunk and by how much it exceeded the limit, giving the PR author a concrete number to investigate instead of a vague "the bundle got bigger" complaint days later.

Here's how to confirm the check is actually blocking merges, not just running informationally:

  1. Open the repo's branch protection settings for main and check that "Bundle Size Check" is listed under required status checks.
  2. Push a throwaway PR that imports a large, unused dependency and confirm the check fails red, not just runs and passes silently.
  3. Confirm the merge button is disabled while the check is red, some teams add the workflow without ever marking it required, which quietly defeats the whole setup.

A size-limit workflow that runs in CI but isn't marked as a required status check in branch protection settings can still fail red on every regression while the merge button stays fully clickable, which defeats the entire point of adding the check in the first place.

Share this Post on X Bluesky

How Do You Post the Size Diff as a PR Comment?

The size-limit GitHub Action posts a comparison comment directly on the PR, showing the size delta against the base branch, which is more actionable during review than a pass/fail check alone:

# Add this step to the same size-check.yml workflow
- uses: andresz1/size-limit-action@v1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}

This surfaces a table like Main bundle: 174.2 KB (+3.1 KB, +1.8%) directly in the PR conversation, so a reviewer sees the size impact of the change without needing to run anything locally or dig through a separate CI log. A size delta is the difference between a chunk's size on the PR branch and its size on the base branch, and surfacing it as a signed number (plus or minus, in both kilobytes and percent) is what makes a two-line dependency bump legible as a 3.1KB regression instead of an invisible line in a lockfile diff. On the project that prompted this whole setup, the delta comment would have shown "+180.4 KB, +112%" directly under the offending pull request, which is a number no reviewer scrolls past without asking a question.

How Do You Choose the Budget Number?

npx size-limit
# Main bundle: 162.4 KB / 180 KB (90%)

Run this against your current main branch first to get a real baseline, then set the budget at roughly 110-120% of that measured number, not a round figure picked without reference to the actual codebase. A budget set with no headroom fails on routine, legitimate growth (a new feature that genuinely needs a few more KB) and trains the team to bump the limit reflexively rather than treating a failure as a signal worth investigating. A budget with too much headroom, on the other hand, catches nothing until the bundle has already grown far past where anyone would want it. In my testing, a 15 percent buffer struck the right balance on three different projects, tight enough that a genuine 180KB regression still failed loudly, loose enough that a normal two-kilobyte feature addition never triggered a false alarm.

What Did It Catch When I Ran It?

I built a throwaway fixture to see the failure message instead of describing it: size-limit 14.1.0, @size-limit/preset-small-lib, lodash-es 4.18.1 and date-fns 4.4.0. The baseline file imported debounce by name and format from date-fns, with an 8 kB budget. The "regressed" file made one change, import _ from 'lodash-es' instead of the named import.

EntryGzipBrotliBudget 8 kB
Named import ({ debounce })6.39 kB5.78 kBpass
Default import (_)35.49 kB31.33 kBfail

One import line took the bundle from 6.4 kB to 35.5 kB gzipped, and it would have looked like a harmless style change in a diff. The default-import form pulled the whole lodash-es namespace into the bundle, while the named form let the bundler drop the rest. size-limit's output for the failing entry was short and specific:

Package size limit has exceeded by 27.49 kB
Size limit: 8 kB
Size:       35.49 kB with all dependencies, minified and gzipped

The process exited with code 1, which is all a CI step needs to turn a pull request red. I'd call that the entire value proposition. Before that exit code existed, the only way to notice was someone opening a bundle analyzer, and nobody does that on a Tuesday.

One more thing from the same run. My first config set "brotli": false, expecting that to mean gzip. It didn't: the entry reported 21.55 kB, which is the minified size with no compression at all. Gzip only turned on once I added "gzip": true. The --json flag prints size, sizeLimit and passed per entry, which is how I caught it.

What Doesn't size-limit Catch?

A per-chunk budget catches growth in the chunks you've configured, it won't catch a new chunk appearing that wasn't in the config at all (a route added without route-level code-splitting, landing entirely inside the main bundle instead of its own chunk), or a regression in a chunk you haven't budgeted. Review the size-limit config itself periodically as the app's chunk structure evolves, an outdated config silently stops protecting the parts of the bundle that changed shape since it was last written. Per the size-limit repository's own documentation, the tool only measures paths matching the glob patterns you've listed, so a build output rename, say from dist/assets/index-*.js to a different hash pattern after a bundler upgrade, can silently produce zero matched files and a check that always passes for the wrong reason.

How Does the Old Way Compare to the Modern Way?

ApproachWhen it catches a regressionEffort to check
Manual bundle analyzerWhenever someone remembers to run itManual, easy to skip
Lighthouse CI performance scoreAfter deploy, aggregated with other metricsAutomated, but not bundle-specific
size-limit in CIAt PR review, before mergeAutomated, zero ongoing effort once configured

What Is the Takeaway?

A bundle size regression is invisible in a normal code review, package.json diffs don't show the transitive dependency tree, and a 180KB jump from a single patch bump reads the same in a diff as a 2KB one. size-limit in CI turns that invisible number into an explicit, enforced check at exactly the point where catching it costs the least, before the PR merges, not three weeks after a user notices.

Frequently Asked Questions

What's the difference between size-limit and a manual bundle analyzer check?
A bundle analyzer (webpack-bundle-analyzer, vite-bundle-visualizer) is a tool you run manually to inspect what's in a bundle, useful for investigating a known problem. size-limit is a CI-integrated tool that enforces a numeric budget automatically on every pull request, failing the build if a bundle exceeds its configured limit, so a regression is caught at the PR review stage instead of relying on someone remembering to check a bundle analyzer periodically.
How do I pick the right size budget number for size-limit?
Start from your current bundle size, measured with size-limit itself, and set the budget slightly above that (10-20% headroom) rather than an arbitrary round number. A budget set too tight fails on routine, legitimate growth and gets ignored or bypassed; a budget set too loose catches nothing. Revisit the number deliberately every quarter or so as the app grows, rather than letting it silently ratchet up every time a PR needs a one-off bump.
Does size-limit measure gzipped or raw bundle size?
It depends on the version. In size-limit 14.1.0 with the small-lib preset, an entry with no compression setting reported its size as minified and brotlied (5.78 kB for my fixture). Add "gzip": true to an entry to get gzip instead (6.39 kB for the same code). Pick the one your CDN actually serves and keep it fixed, because switching mid-project shifts every number by roughly 10 percent.