Speed Up Next.js in GitHub Actions
Last updated
The fastest way to speed up Next.js in GitHub Actions is to persist .next/cache between runs and to stop installing and building the same things in every job. A cold next build recompiles and re-optimizes everything. A warm one reuses the compiler and image caches and only does the work for files that changed.
This guide covers the real costs in a Next.js pipeline: install, build, type-check, unit tests and end-to-end tests. It is the Next.js version of our general guide to speeding up GitHub Actions.
Where the time goes
A typical Next.js workflow does five things: install dependencies, lint, type-check, build, and test. On a 2-vCPU runner the build is often the longest step, and next build warns you when it is slow for a fixable reason: "No Cache Detected". That message means .next/cache was empty. Next.js documents this in its CI build caching guide.
Look at your per-step timings first. If install takes 60 seconds and the build takes three minutes, fix the build cache before anything else.
Cache .next/cache and the package manager
Next.js saves its compiler and optimization output in .next/cache. The official guide shows a key built from the lockfile hash plus a hash of the source files, with a restore-keys fallback. That fallback matters: when you change a source file, the exact key misses, but the restore key brings back the previous cache so only changed files rebuild.
name: ci
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- uses: actions/cache@v6
with:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- run: npm ci
- run: npm run build
Two details. First, actions/setup-node with cache: npm caches the npm download cache, not node_modules, so npm ci still does a clean install but skips the downloads. Use cache: pnpm or cache: yarn for those managers. Second, GitHub cache entries are immutable: a run can only save a key that does not exist yet. A key that includes a source hash creates a new entry on nearly every PR, which fills your quota. See GitHub Actions cache limits for eviction rules, and use the cache key builder to try other keys.
Split the work into parallel jobs
Lint, type-check and unit tests do not need the build output. Run them as separate jobs so they overlap with the build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx eslint . --cache --cache-location .eslintcache
- run: npx tsc --noEmit --incremental
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx vitest run
Each job repeats checkout and install, so this only pays off while install is cached and short. If your unit tests are the slow part, see Jest slow or Vitest slow.
Turbopack and build time
Newer Next.js releases use Turbopack, a Rust bundler, for next dev and next build. Check your version's release notes for whether next build uses it by default, and for the flags to opt in or out, since this has changed between releases. Turbopack is multi-threaded, so it benefits from more cores than the 2 on a default hosted runner. It does not remove the need for .next/cache. Measure next build with and without it on your own app; do not assume.
Other build-time costs worth checking:
- Telemetry. Set
NEXT_TELEMETRY_DISABLED=1in CI so the build makes no extra calls. - Static generation that calls APIs. If
generateStaticParamshits a slow backend, the build waits on it. Point CI at a fast fixture or mock. - Image optimization. Many unique remote images slow the build. The cache helps on repeat runs.
- Source maps. Production browser source maps add time. Disable them in CI builds you only use for tests.
End-to-end tests with Playwright
Playwright is usually the longest job in a Next.js repo. Build once, upload the .next output with actions/upload-artifact, and run next start in each shard rather than rebuilding per shard. Use Playwright's --shard=1/4 flag with a matrix. The full recipe, including the browser cache, is in Playwright tests slow and test sharding in GitHub Actions. The test shard calculator helps pick a shard count.
What more CPU cores buy you
next build, tsc, Turbopack and Playwright workers all use several cores. A 2-vCPU runner leaves much of that unused. Our benchmarks include two Next.js stacks, run on 8 October 2026 as full CI runs with 10 test shards and a real database. Turborepo + Next.js + tRPC + MySQL went from 3m29s on a GitHub-hosted runner to 1m53s on compiler.dev (1.9x). Next.js + Supabase with Playwright went from 4m03s to 2m48s (1.4x). Those are whole pipelines, not just the build step, so treat them as an indication, not a promise for your repo.
To try it, switching is one line, runs-on: [self-hosted, Linux, compiler-ci], after you install the GitHub App. The default machine is 8 vCPU and 16 GB at $0.012 per minute, and jobs fall back to your original runners if anything fails. Details are on the pricing page and in the comparison. There is also a Next.js template and a general Node template.
Checklist
- Add
actions/cachefor.next/cachewith a lockfile plus source key and a restore key. - Use
setup-nodecaching for the package manager. - Run lint, type-check, unit tests and build as parallel jobs.
- Build once for Playwright and shard the tests.
- Add
concurrencyandtimeout-minutes. - Try more cores for the build job and compare cost per run.
FAQ
Why does next build say "No Cache Detected"?
.next/cache was empty at build time. Persist that folder between runs with actions/cache, as shown above, and the message goes away on the next run.
Should I cache node_modules in GitHub Actions?
Usually no. Cache the package manager's download cache through actions/setup-node and run a clean npm ci. A restored node_modules can hide lockfile problems.
Does Turbopack make caching unnecessary?
No. Turbopack makes compilation faster, but .next/cache still stores results and speeds up repeat builds. Measure both on your app.
How do I speed up Next.js builds in a monorepo?
Build only affected apps and use a remote cache. See monorepo CI with Nx or Turborepo.
Related
- Add a CI Speed Badge to Your READMEShow how fast your CI is with a README badge: the median GitHub Actions run time for your repository, updated automatically. Free for public repos.
- Speed Up .NET in GitHub ActionsSpeed up .NET builds in GitHub Actions: NuGet caching with setup-dotnet, --no-restore, --no-build, parallel dotnet test, and SQL Server services.
- Speed Up Bazel in GitHub ActionsSpeed up Bazel in GitHub Actions with setup-bazel, a disk cache, repository cache, a remote cache, and the right --jobs setting. Working YAML.
- Speed Up Django in GitHub ActionsSpeed up Django tests in GitHub Actions with pip or uv caching, pytest-xdist, --reuse-db, a Postgres service, and test splitting. Working YAML.