Skip to content
compiler.dev

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=1 in CI so the build makes no extra calls.
  • Static generation that calls APIs. If generateStaticParams hits 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

  1. Add actions/cache for .next/cache with a lockfile plus source key and a restore key.
  2. Use setup-node caching for the package manager.
  3. Run lint, type-check, unit tests and build as parallel jobs.
  4. Build once for Playwright and shard the tests.
  5. Add concurrency and timeout-minutes.
  6. 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.

Run this workflow on compiler.dev 200 free minutes

Start free