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:
- Open the repo's branch protection settings for
mainand check that "Bundle Size Check" is listed under required status checks. - Push a throwaway PR that imports a large, unused dependency and confirm the check fails red, not just runs and passes silently.
- 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.
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.
| Entry | Gzip | Brotli | Budget 8 kB |
|---|---|---|---|
Named import ({ debounce }) | 6.39 kB | 5.78 kB | pass |
Default import (_) | 35.49 kB | 31.33 kB | fail |
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?
| Approach | When it catches a regression | Effort to check |
|---|---|---|
| Manual bundle analyzer | Whenever someone remembers to run it | Manual, easy to skip |
| Lighthouse CI performance score | After deploy, aggregated with other metrics | Automated, but not bundle-specific |
| size-limit in CI | At PR review, before merge | Automated, 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.