Configuring size-limit as a GitHub Action

Scenario: a team ships a redesigned settings page. The PR passes tests and the visual diff looks fine, so it merges. Two weeks later someone runs a manual check and notices the vendor chunk grew:

$ du -sh dist/assets/vendor-*.js
96K   dist/assets/vendor-Xa1b2c.js   (before, last known-good tag)
141K  dist/assets/vendor-Yc3d4e.js   (current main)

Nobody caught the 45 KB increase because nobody was looking — reviewers approve functional diffs, not dist/ output, and no automated signal surfaced the number anywhere a human would see it during review. This page wires size-limit into a GitHub Action so that number appears as a bot comment on every PR, before merge, with no manual du -sh required.

Root cause: size measurement with no visible feedback loop

The underlying problem here isn’t a missing tool — most teams that hit this already have size-limit installed and running as a pass/fail CI check, which is the topic of the parent enforcing performance budgets in CI guide. A pass/fail check answers “did this PR violate the hard ceiling,” but it doesn’t answer “did this PR make things measurably worse while staying under the ceiling,” and a PR that grows a chunk from 96 KB to 141 KB while the budget is 150 KB passes silently. The fix is not a stricter threshold — it’s visibility. Posting the actual delta as a PR comment turns a binary gate into a piece of information every reviewer sees, the same way a code-coverage bot comment does for test coverage.

This complements vendor chunk isolation and third-party management — isolating vendor code into its own chunk only pays off if someone notices when that chunk starts growing again. It also compounds with dead-code elimination work: a team that has already invested in advanced tree-shaking and dependency optimization has less slack in its budget to absorb an accidental regression, which makes the PR-comment feedback loop more valuable, not less — there is no longer 60 KB of dead weight quietly hiding a 15 KB mistake.

The glob patterns in the configuration below only resolve correctly if chunk filenames are stable enough for size-limit to find the same logical chunk build after build. If your hashing strategy produces a different filename for the vendor chunk on every commit regardless of whether its contents changed, see deterministic chunk hashing for long-term caching before relying on the glob-based approach here — an unstable hash doesn’t break size-limit’s measurement, but it does make the base-branch comparison less trustworthy from run to run.

Exact configuration

1. Install dependencies

# Webpack 5 / Vite 5+ — same size-limit package for both
npm install --save-dev size-limit @size-limit/file @size-limit/preset-app

Use @size-limit/file for simple path-based glob checks (what this page covers); use @size-limit/preset-app if you want size-limit to also account for import-cost analysis of your entry file rather than just measuring built output.

2. .size-limit.json — one entry per chunk type

// .size-limit.json — Webpack 5 / Vite 5+ (adjust glob to your output.filename pattern)
[
  {
    "name": "Entry chunk (gzip)",
    "path": "dist/assets/index-*.js",
    "limit": "150 KB",
    "gzip": true
  },
  {
    "name": "Entry chunk (brotli)",
    "path": "dist/assets/index-*.js",
    "limit": "130 KB",
    "brotli": true
  },
  {
    "name": "Vendor chunk (gzip)",
    "path": "dist/assets/vendor-*.js",
    "limit": "80 KB",
    "gzip": true
  },
  {
    "name": "CSS bundle (gzip)",
    "path": "dist/assets/index-*.css",
    "limit": "40 KB",
    "gzip": true
  }
]

Brotli typically comes in 10-15% smaller than gzip at the same content, which is why the brotli limit above is tighter than the gzip limit for the same chunk — they are two views of the same file, not two different budgets. Pick whichever compression your production CDN actually negotiates as the number that gates the merge, and keep the other as an informational row.

3. package.json script

// package.json
{
  "scripts": {
    "build": "vite build",
    "size": "size-limit"
  },
  "devDependencies": {
    "size-limit": "^11.1.4",
    "@size-limit/file": "^11.1.4"
  }
}

4. GitHub Actions workflow with automatic PR comments

# .github/workflows/size-limit.yml — Webpack 5 / Vite 5+
name: Size Limit
on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write   # Required for the bot to post/update the PR comment

jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - name: Build
        run: npm run build
      - name: size-limit comment
        uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          skip_step: build   # We already built above; don't let the action rebuild

The action runs size-limit internally, captures the JSON output, fetches the same measurement from the base branch’s most recent successful run (cached via the action’s own artifact strategy), and posts a single Markdown table comment — updating the same comment on subsequent pushes rather than spamming a new one each time.

Reading the posted comment

The andresz1/size-limit-action renders a Markdown table with one row per .size-limit.json entry. A typical comment on a PR that regresses the vendor chunk looks like this once GitHub renders it:

Path                    Size      Loading time (3g)   Running time (snapdragon)   Total time
Entry chunk (gzip)      148.9 KB  +0 B   3s          70ms        3.1s
Vendor chunk (gzip)     124.6 KB  +44.6 KB (▲55.75%)  2.4s        58ms        2.5s
CSS bundle (gzip)       31.2 KB   -0.4 KB (▼1.27%)     0.6s        —           0.6s

The delta column and percentage change are what turn this from a static snapshot into a review signal — a reviewer scanning the diff sees “vendor chunk +44.6 KB / 55.75%” attached to the exact PR that caused it, without needing to check out the branch or run a bundle analyzer locally. If the number looks surprising, the next step is almost always to open reading webpack-bundle-analyzer treemaps or analyzing chunks with source-map-explorer against that specific chunk to find which import is responsible before deciding whether the growth is justified.

Step-by-step verification

  1. Merge the workflow file to the default branch first — GitHub Actions triggered by pull_request on a new workflow file only runs once that file exists on the base branch, not just the feature branch.
  2. Open a throwaway PR that adds an unused but real import (import 'chart.js') to confirm the pipeline actually measures a change: git checkout -b size-limit-smoke-test.
  3. Push the branch and open the PR; watch the Actions tab for the Size Limit workflow to go green.
  4. Confirm the bot comment appears on the PR with a Markdown table listing each .size-limit.json entry, its current size, and (after a second push) a delta column.
  5. Push a second, unrelated commit to the same PR and confirm the same comment updates in place rather than a duplicate comment appearing — this confirms github_token permissions and the action’s comment-matching logic are both working.
  6. Revert the throwaway import, push again, and confirm the delta column shows a negative number matching the import’s approximate size.
  7. Merge the workflow to main and enable it as a required status check in branch protection settings so the size job cannot be skipped.

Edge cases and gotchas

Gotcha 1 — forked-repo PRs get read-only tokens

Pull requests from forks (common in open-source projects) run under a GITHUB_TOKEN with read-only permissions by default, regardless of what the workflow’s permissions block requests, unless the repository owner has explicitly enabled “Read and write permissions” for fork workflows in repository settings. The build and measurement steps still succeed, but the comment-posting step fails silently or with a 403 buried in the Action log. Fix: for public repositories accepting external contributions, either enable write permissions for fork PRs in Settings → Actions → General, or split the workflow into a pull_request_target variant that runs with base-repo permissions (with the security caveats that trigger implies).

Gotcha 2 — glob patterns don’t match hashed filenames after a build tool change

A glob like dist/assets/index-*.js assumes a specific [name]-[hash].js output pattern. Switching from Webpack’s default [name].[contenthash:8].js to Vite’s [name]-[hash].js (or changing either bundler’s output.filename config) silently breaks the glob, and size-limit reports “No files found” instead of an error that would be obvious in a PR comment. Fix: after any change to output.filename / build.rollupOptions.output, re-run npx size-limit locally and confirm each entry’s reported size is non-zero before pushing.

Gotcha 3 — measuring the wrong compression for a brotli-only CDN

If your CDN only serves brotli (no gzip fallback negotiated), a .size-limit.json entry using "gzip": true measures a compression format that never actually reaches a browser, understating real payload savings and overstating the safety margin against the 150 KB target. Fix: confirm your CDN’s Content-Encoding response header with curl -H "Accept-Encoding: br" -I https://your-domain.com/assets/index-*.js and set the size-limit entries to match what that header actually reports.

Gotcha 4 — one .size-limit.json doesn’t scale to a multi-app monorepo

A single flat .size-limit.json at the repo root works for one application, but a monorepo with three or four independently-deployed apps sharing a workspace ends up with either one bloated config file mixing unrelated chunk names, or — worse — one app’s regression silently absorbed into another app’s headroom if the globs are written loosely enough to match across package boundaries. Fix: place a .size-limit.json inside each app’s own package directory, and run the size-limit step once per app in a matrix job:

# .github/workflows/size-limit.yml — matrix variant for a monorepo
jobs:
  size:
    strategy:
      matrix:
        app: [marketing-site, dashboard, admin-console]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run build --workspace=${{ matrix.app }}
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          directory: apps/${{ matrix.app }}
          skip_step: build

Each matrix entry posts its own status check and its own row in the PR comment thread, which keeps a regression in admin-console from ever being able to hide behind headroom in marketing-site.


FAQ

Should the size-limit GitHub Action compare against brotli or gzip?

Match whatever your CDN or origin server actually serves in production. Most modern CDNs negotiate brotli for supporting browsers and fall back to gzip, so brotli is the more representative number for real user payload — but gzip remains the safer default when you are not certain what your infrastructure serves, since it is universally supported and slightly overstates size rather than understating it.

Why didn’t the size-limit action post a comment on my PR?

The most common cause is a missing pull-requests: write permission on the workflow or job. GitHub Actions default to read-only permissions on pull_request-triggered workflows from forked repositories, which silently blocks the comment API call without failing the build step itself.

Can size-limit check multiple chunk paths in one PR comment?

Yes. Each entry in the size-limit array becomes its own row in the posted comment, with its own name, path glob, and limit. This is the recommended way to track entry, vendor, and route chunks independently rather than summing them into a single number.

Does size-limit need the production build to run first?

Yes. size-limit only measures files that already exist on disk — it does not invoke webpack or vite itself unless you use the running-mode plugins. The GitHub Actions workflow must run npm run build (or the equivalent vite build / webpack command) before the size-limit step, or every glob will resolve to zero matched files.