A GitHub Action that tracks Next.js App Router bundle sizes across pull requests. It stores a baseline for each branch and posts a route-by-route size comparison comment on every PR.
- name: Analyze bundle sizes
uses: michalsanger/nextjs-turbopack-bundle-size@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
minimum-change-threshold: 256
budget-percent-increase-red: 20
app-name: My AppThe action must run after the app has been built with TURBOPACK_STATS=1. See examples/usage.yml for a complete single-app workflow, or examples/monorepo.yml for a matrix workflow covering multiple apps in a monorepo.
The action posts a comment like this on every pull request:
This analysis was generated by the Next.js Turbopack Bundle Size action. 🤖
Route Size (gzipped) First load Diff (vs baseline) global214.5 KB— 🟡 +1.2 KB/85.2 KB299.7 KB🟢 -1.5 KB/about42.3 KB256.8 KB🔴 +3.1 KB/blog38.7 KB253.2 KB➖ No change /dashboard51.4 KB265.9 KB🟡 +800 B/settings29.1 KB243.6 KB🆕 New /old-page— — 🗑️ Removed
| Input | Required | Default | Description |
|---|---|---|---|
github-token |
Yes | — | GitHub token for downloading baseline artifact and posting PR comments |
stats-path |
No | .next/diagnostics/route-bundle-stats.json |
Path to the Turbopack stats file. Auto-detects legacy path .next/server/webpack-stats.json if the default doesn't exist. |
artifact-name |
No | turbopack-main-stats |
Artifact name prefix for storing baseline stats. The branch name is appended automatically (e.g. turbopack-main-stats-main). |
minimum-change-threshold |
No | 0 |
Byte threshold below which a size change is considered unchanged. For example, 500 means changes of 500 B or less are shown as "➖ No change". |
budget-percent-increase-red |
No | 0 |
Percentage threshold for flagging size increases as critical. Increases above this percentage show 🔴, others show 🟡. Default 0 means all increases show 🔴. For example, 20 means only increases above 20% are flagged red. |
app-name |
No | — | Application name in the report header (e.g. My App → "📦 My App — App Router Sizes (Turbopack)"). When set, the sticky PR comment uses bundle-size-report-{name} so matrix jobs for multiple apps do not overwrite each other. If not set, a generic header and the default comment key bundle-size-report are used. |
baseline-workflow |
No | — | Workflow file name (e.g. main.yml) to look up the baseline artifact in. Set this when the baseline artifact is uploaded by a different workflow than the one running this action. When unset, the artifact is searched in the current workflow. |
permissions:
contents: read
pull-requests: write
actions: read- On push to any branch: parses the stats file, computes gzip sizes for each route, and uploads the result as a GitHub Actions artifact (per branch).
- On pull request: downloads the baseline artifact from the PR's target branch, parses the current stats file, calculates gzip sizes, and posts (or updates) a sticky comment with a route-by-route comparison table.
A PR is compared against a baseline stored for its target branch, so that branch needs a baseline — which means your workflow must run on pushes to it. The examples trigger on pushes to main, which covers the common case of PRs into main. To also compare PRs that target another long-lived branch (e.g. develop), add it to the push triggers:
on:
push:
branches: [main, develop]
pull_request:If no baseline exists for the target branch, the action falls back to the default branch's baseline (and, for repos upgrading from an older version, the previous unsuffixed artifact name).
Notes: Branch names are slugified into the artifact name (any character outside
A-Za-z0-9._-, such as/, becomes-), so two branches differing only by such characters (e.g.feat/aandfeat-a) would share a baseline. Each branch's baseline is kept under GitHub's default artifact retention (90 days); deleted branches leave an orphaned artifact until it expires.
Chunk files and the app-paths manifest are resolved relative to the .next directory inferred from stats-path, so pointing it to a subdirectory (e.g. apps/my-app/.next/diagnostics/route-bundle-stats.json) works correctly without any additional configuration. See examples/monorepo.yml for a complete monorepo setup.
Note: If your workflow uses
defaults.run.working-directoryto set a subdirectory (e.g.my-app), be aware that this only applies torun:steps — it does not apply touses:steps like this action. You must include the subdirectory instats-path:stats-path: my-app/.next/diagnostics/route-bundle-stats.json
The first PR before any baseline exists will show all routes as "🆕 New" — this is expected.
Inspired by nextjs-bundle-analysis by HashiCorp.