GitHub Actions Performance: 10 Tips to Speed Up Your CI/CD

A 10-minute CI pipeline that could run in 3 minutes is costing your team hours every week. Multiply that across dozens of developers and hundreds of commits, and you're looking at serious productivity losses. Here's how to optimize your GitHub Actions workflows for speed.
1. Cache Dependencies Aggressively
The single biggest win for most projects. Instead of downloading dependencies on every run:
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
Language-specific cache paths:
| Language | Cache Path |
|---|---|
| Node.js | ~/.npm or node_modules |
| Python | ~/.cache/pip |
| Go | ~/go/pkg/mod |
| Rust | ~/.cargo and target |
| Java/Maven | ~/.m2/repository |
Pro tip: Use hashFiles() on lock files to invalidate cache only when dependencies actually change.
2. Use Parallel Jobs with Matrix Strategy
Run tests across multiple versions or configurations simultaneously:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Set fail-fast: false to continue other jobs even if one fails - useful for seeing the full picture of what's broken.
3. Split Tests into Parallel Shards
For large test suites, split tests across multiple runners:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --shard=${{ matrix.shard }}/4
Many test frameworks support sharding natively (Jest, pytest, RSpec). A test suite that takes 20 minutes can run in 5 minutes with 4 shards.
4. Use Smaller Runner Images
The default ubuntu-latest includes tons of pre-installed software. If you don't need it:
jobs:
build:
runs-on: ubuntu-24.04 # Specific version, more predictable
container:
image: node:20-slim # Minimal image with just what you need
Or use a custom Docker image with your dependencies pre-installed.
5. Skip Unnecessary Steps with Conditionals
Don't run deployment on every PR:
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh
- name: Run E2E tests
if: contains(github.event.pull_request.labels.*.name, 'e2e')
run: npm run test:e2e
Use path filters to skip workflows entirely when irrelevant files change:
on:
push:
paths:
- 'src/**'
- 'package.json'
- '.github/workflows/**'
paths-ignore:
- '**.md'
- 'docs/**'
6. Optimize Docker Builds
Docker builds are often the slowest step. Use BuildKit and layer caching:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
The type=gha cache uses GitHub's cache storage for Docker layers.
7. Use Artifacts for Job Dependencies
Pass build outputs between jobs instead of rebuilding:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: ./deploy.sh
8. Consider Self-Hosted Runners
For very frequent builds or specific hardware needs:
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- run: make build
Benefits:
- No queue wait times
- Persistent caches (no download time)
- Custom hardware (GPUs, ARM, more RAM)
- Cost savings at scale
Use actions/runner or managed solutions like Buildjet or Namespace.
9. Avoid Unnecessary Checkouts
The default checkout fetches full history. For most CI jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 1 # Shallow clone, just latest commit
For monorepos with sparse checkout:
- uses: actions/checkout@v4
with:
sparse-checkout: |
packages/my-app
shared/
10. Profile Your Workflows
You can't optimize what you don't measure. Use job summaries and timing:
- name: Build
run: |
start=$(date +%s)
npm run build
end=$(date +%s)
echo "Build took $((end-start)) seconds" >> $GITHUB_STEP_SUMMARY
Or use the Actions Timing Insights action for detailed breakdowns.
Quick Wins Checklist
| Optimization | Typical Savings |
|---|---|
| Dependency caching | 30-60% |
| Parallel test shards | 50-75% |
| Shallow checkout | 5-10% |
| Path filters | 100% (skipped runs) |
| Docker layer caching | 40-70% |
| Self-hosted runners | 20-50% (no queue) |
Common Pitfalls to Avoid
- Cache key too broad: Use specific keys with lock file hashes
- Not using
npm ci: It's faster thannpm installfor CI - Running all tests on every PR: Use path filters and labels
- Sequential jobs that could parallel: Review job dependencies
- Large artifacts: Compress before upload, clean up after use
Example: Optimized Node.js Workflow
name: CI
on:
push:
branches: [main]
paths-ignore: ['**.md']
pull_request:
paths-ignore: ['**.md']
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: build
path: dist/
retention-days: 1
FAQ
How much does caching really help?
For a typical Node.js project, caching node_modules can reduce install time from 60+ seconds to under 5 seconds.
Are self-hosted runners worth it?
If you're running 1000+ minutes/month of Actions, self-hosted runners often pay for themselves. Plus, no queue wait times.
Can I cache everything?
Cache storage is limited (10GB per repo). Cache only what's slow to recreate and changes infrequently.
Want to automate CI/CD monitoring and get alerts when builds slow down? Akmatori helps SRE teams track pipeline performance and catch regressions before they impact developer productivity.
