"CI is too slow" is the most frequent complaint in growing teams. On our projects we cut pipelines from 14 minutes to 3 by applying these seven patterns.
1. npm/pnpm/yarn cache
actions/setup-node has built-in cache: 'pnpm'. For projects with 600+ deps: 3 minutes → 35 seconds on install.
2. Next.js / Vite build cache
Cache .next/cache or node_modules/.vite:
- uses: actions/cache@v4
with:
path: .next/cache
key: nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.{ts,tsx}') }}
3. Parallel jobs, not serial
Lint, type-check, test, build are independent. Stop chaining them. Separate jobs running in parallel: −50% wall time.
4. Test sharding
Vitest and Jest support --shard. With a 4-shard matrix, an 8-minute suite runs in 2.
5. Skip useless jobs with paths-filter
If a PR only touches docs/, why run the whole CI? With dorny/paths-filter we filter early and skip unnecessary jobs.
6. Self-hosted runner for heavy builds
For projects with builds above 10 minutes on standard runners, a self-hosted runner (even Hetzner CX31 at €12/mo) can halve the time. Annual cost < one month of GitHub Pro extra minutes.
7. Per-branch concurrency lock
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Successive pushes cancel previous builds — less waste, fresher deploys.
Bonus
Always measure before optimising. The Actions panel shows step-by-step time. Often the problem is 1 step out of 12 — not 12 steps to shorten.