buildstats.io

Track bundle size over time in GitHub Actions

buildstats records the bundle size your CI measures on every commit, charts it as an SVG in your README and comments the delta on pull requests. Free for public repos, no signup; private $9/month.

Tools like size-limit and compressed-size-action compare a pull request against its base branch and then forget the number. buildstats keeps it: every push to main becomes a point on a chart, every pull request gets a comment with the change against the base branch, and max-regression fails the step when a bundle grows more than you allow.

Setup: three steps in one workflow

buildstats does not build your project. Your job builds it the way it already does, measures the output with wc or stat, and hands the numbers to the bitgate/buildstats@v1 Action. The Action signs in with the workflow's GitHub OIDC token, so there is no API key to create and no secret to store.

name: Build

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  id-token: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build

      - id: size
        run: |
          echo "raw=$(cat dist/assets/*.js | wc -c)" >> "$GITHUB_OUTPUT"
          echo "gzip=$(cat dist/assets/*.js | gzip -9c | wc -c)" >> "$GITHUB_OUTPUT"

      - uses: bitgate/buildstats@v1
        with:
          metrics: |
            bundle=${{ steps.size.outputs.raw }} bytes lower
            bundle_gzip=${{ steps.size.outputs.gzip }} bytes lower
          max-regression: 5

Each metric line is name=value [unit] [higher|lower]. The direction is what turns a delta into a verdict: lower means a smaller bundle is better, so growth shows up red in the comment and counts against max-regression. Up to 50 metrics fit in one push, so the main chunk, the vendor chunk and the CSS can all go in the same step.

The first push from a public repository creates the project at buildstats.io/<owner>/<repo>. Nobody has to sign up first. Sign in with GitHub later to claim the project when you want to manage it, invite members or delete data.

What a pull request gets

On pull_request runs the Action compares every metric against the latest value on the base branch and keeps one comment per project up to date. Matrix jobs share that comment instead of posting one each. The comment lists, per metric, the base value, the pull request value, the delta in units and in percent, and a better or worse verdict.

max-regression: 5 fails the step when a metric with a direction got worse than base by more than 5%. The comment and the job summary are written first, so the numbers are there when you look at the failed check. Make the check required in branch protection and the pull request cannot merge until the bundle is back under the line. Metrics without a base value yet never trip the gate.

Pull requests from forks get no OIDC token, so the Action skips the push there. To record fork pull requests, upload the numbers as an artifact and push them from a workflow_run workflow, which runs in your repository with its own token.

The chart in your README

Every metric has an SVG chart at a stable URL. GitHub renders it through its image proxy, and the image is cached for 5 minutes, so a new push shows up within minutes without a commit to the README.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://buildstats.io/acme/app/bundle_gzip.svg?theme=dark">
  <img alt="bundle_gzip over time" src="https://buildstats.io/acme/app/bundle_gzip.svg">
</picture>

Query parameters pick the view: n for the number of points (30, 50, 100, 200, 500 or 1,000; default 200), w for the width (400 to 1,200 pixels; default 800), ref for a branch other than the default branch and theme=dark for the dark variant. The badge at /bundle_gzip/badge.svg shows the latest value in the shields.io flat style, and /badge.json serves the same number in the shields endpoint format.

More than one bundle

Push bundle/main, bundle/vendor and bundle/css and they draw as separate lines on one chart; up to 8 variants share a palette. The variant input does the same for matrix builds, storing every metric as name/variant, for example bundle_gzip/node22. Any numeric value works the same way, so test counts, coverage or build time can live in the same project as the bundle sizes.

Limits and pricing

Public repositoriesPrivate repositories
PriceFree$9/month per GitHub user or organization
TrialNot needed14 days, no card, starts at the owner's first private push
Projects per owner10025
Metrics per project100100
History per metric and branch50,000 points50,000 points
Metrics per push5050
Pushes per minute and project6060
MembersUnlimitedUnlimited
Chart and badge imagesPublic URLsUnguessable token URLs
Data after the plan lapsesKeptKept 30 days, then deleted

Charts come in light and dark, 400 to 1,200 pixels wide, with 30 to 1,000 points. Pull request refs are kept for 90 days after the last push; branch history stays as long as the project exists. Everything above is on the pricing page.

How this compares with size-limit and bundlewatch

size-limit, compressed-size-action and bundlesize run inside the pull request and compute the diff on the spot; they store nothing, so there is no chart and no answer to "when did the bundle cross 200 kB". bundlewatch keeps the latest value per branch in its service, which is enough for a status check but not for history. RelativeCI and BundleMon keep history, as hosted services tied to a specific bundler output. buildstats stores plain numbers, which is why it works for Rollup, esbuild, Vite, webpack, Next.js and anything else that writes files. The comparison of bundle size tools lists each tool's storage, pull request output and price side by side.

Questions

Does buildstats build or analyze the bundle?

No. Your workflow builds the project and measures the files with wc, stat, gzip or brotli. buildstats stores the resulting numbers, charts them and compares them on pull requests. There is nothing to configure per bundler.

Does it work with Next.js, Vite, webpack or esbuild?

Yes. Anything that writes files works, because the measurement is a shell command over the output directory. For Next.js measure .next/static, for Vite measure dist/assets, for a library measure the single file you publish.

Can I use it outside GitHub Actions?

Yes. Create a project API key and POST the same metrics to the API with curl from GitLab CI, CircleCI, a Makefile or a local script. The GitHub Action is a thin shell wrapper around that call.

What does the pull request comment compare against?

The latest stored value on the base branch of the pull request, or the branch you pass in base-ref. If that branch has no value for a metric yet, the comment shows no change and max-regression ignores the metric.

Where is the data stored and can I delete it?

On buildstats.io, keyed by repository. Members can delete single points, whole metrics or the project from the project page or the API at any time.

What happens on a private repository?

The first private push by an owner starts a 14-day trial with no card. After that, Pro is $9/month per GitHub user or organization and covers 25 private projects. If the plan lapses, pushes are acknowledged but not stored and the data is deleted after 30 days.

Last updated 2026-10-07.