A slow CI pipeline changes how a team works. People batch changes, skip waiting for checks and context-switch while builds run. With AI agents opening more pull requests than ever, CI speed matters even more.
Here are the changes that usually make the biggest difference, in rough order of payoff.
1. Cache dependencies
Installing dependencies from scratch on every run is the most common waste. The setup-* actions have built-in caching:
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfileThe cache key is based on your lockfile, so it is invalidated only when dependencies change.
2. Cache build outputs
Tools like Turborepo, Nx, Gradle and Bazel can cache task outputs. Combined with a remote or Actions cache, unchanged packages are not rebuilt or retested.
3. Run jobs in parallel
Split independent work into separate jobs that run at the same time:
jobs:
lint: { runs-on: ubuntu-latest, steps: [ ... ] }
typecheck: { runs-on: ubuntu-latest, steps: [ ... ] }
test: { runs-on: ubuntu-latest, steps: [ ... ] }4. Shard slow test suites
Use a matrix to split tests across runners:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx vitest run --shard=${{ matrix.shard }}/45. Only run what changed
- Use
pathsfilters so documentation changes do not run the full test suite. - In monorepos, run tasks only for affected packages.
on:
pull_request:
paths: ["src/**", "package.json", "pnpm-lock.yaml"]6. Cancel outdated runs
When you push again, the previous run is usually obsolete:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true7. Speed up Docker builds
Use BuildKit layer caching with the GitHub Actions cache backend:
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=max8. Use bigger runners where it counts
For CPU-heavy builds and tests, larger runners can cost less overall because they finish faster. Measure before and after.
Keep it secure while you optimise
- Pin third-party actions to a commit SHA.
- Set minimal
permissions:for theGITHUB_TOKEN. - Be careful with caches and
pull_request_targeton public repositories, which can expose secrets to untrusted code.
Key takeaways
- Cache dependencies and build outputs first.
- Parallelise jobs and shard slow test suites.
- Skip unaffected work and cancel superseded runs.
- Optimise without loosening permissions or pinning.