Optimizing GitHub Actions Performance
Workflow execution time directly impacts developer productivity and CI/CD costs. Learn strategies to optimize workflows using caching, artifacts, and smart parallelization.
Dependency Caching
Cache npm dependencies to avoid re-downloading on every run:
name: Fast Build with Caching
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Automatic npm caching
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
Custom Cache Paths
Cache specific directories for maximum speed:
steps:
- uses: actions/checkout@v4
- name: Cache node modules
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
.next/cache
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
Playwright Browser Caching
Cache Playwright browsers to save installation time:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Get Playwright version
id: playwright-version
run: echo "version=$(npm list @playwright/test --depth=0 | grep @playwright | sed 's/.*@//')" >> $GITHUB_OUTPUT
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- name: Run tests
run: npm run test:e2e
Build Artifact Caching
Share build outputs between jobs:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install and build
run: |
npm ci
npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 1
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Test built site
run: npm run test:e2e
Conditional Job Execution
Skip unnecessary jobs based on file changes:
jobs:
check-changes:
runs-on: ubuntu-latest
outputs:
docs: ${{ steps.filter.outputs.docs }}
src: ${{ steps.filter.outputs.src }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
docs:
- 'docs/**'
- '*.md'
src:
- 'src/**'
- 'package.json'
test:
needs: check-changes
if: needs.check-changes.outputs.src == 'true'
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
docs:
needs: check-changes
if: needs.check-changes.outputs.docs == 'true'
runs-on: ubuntu-latest
steps:
- name: Build docs
run: npm run docs:build
Concurrency Control
Prevent redundant workflow runs:
name: Deploy
on:
push:
branches: [main]
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true # Cancel previous runs
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: ./deploy.sh
Optimized Blog Workflow
Real-world optimized workflow for the blog:
name: Optimized CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest
outputs:
tests: ${{ steps.filter.outputs.tests }}
content: ${{ steps.filter.outputs.content }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
tests:
- 'src/tests/**'
- 'playwright.config.ts'
content:
- 'src/content/**'
- 'public/**'
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js with caching
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Cache Astro build
uses: actions/cache@v4
with:
path: |
.astro
dist
key: astro-build-${{ hashFiles('src/**', 'astro.config.mjs') }}
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 1
test:
needs: [build, changes]
if: needs.changes.outputs.tests == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Cache Playwright
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Download build
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Run tests (shard ${{ matrix.shard }})
run: npx playwright test --shard=${{ matrix.shard }}/4
deploy:
needs: [build, test]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Deploy to Vercel
run: npx vercel deploy --prod --prebuilt
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Cache Debugging
Monitor cache effectiveness:
steps:
- name: Restore cache
id: cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
- name: Cache status
run: |
if [ "${{ steps.cache.outputs.cache-hit }}" == "true" ]; then
echo "✅ Cache hit! Saved time." >> $GITHUB_STEP_SUMMARY
else
echo "❌ Cache miss. Installing dependencies..." >> $GITHUB_STEP_SUMMARY
fi
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
Smart Workflow Triggers
Optimize when workflows run:
on:
push:
branches: [main]
paths:
- 'src/**'
- 'package.json'
- '.github/workflows/**'
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch: # Manual trigger
schedule:
- cron: '0 2 * * 1' # Weekly on Monday at 2 AM
jobs:
test:
# Skip if commit message contains [skip ci]
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
Parallel Job Execution
Maximize parallelization:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run typecheck
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:unit
# All above run in parallel
e2e:
needs: [lint, typecheck, unit-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run test:e2e
Workflow Timing Analysis
Track performance:
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- name: Start timer
id: start
run: echo "time=$(date +%s)" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4
- name: Setup and install
run: |
npm ci
npm run build
- name: Calculate duration
run: |
END=$(date +%s)
DURATION=$((END - ${{ steps.start.outputs.time }}))
echo "## ⏱️ Build took ${DURATION}s" >> $GITHUB_STEP_SUMMARY
Best Practices
- Always cache dependencies - Speeds up by 50-80%
- Use artifacts for build outputs - Share between jobs
- Implement concurrency control - Cancel outdated runs
- Filter paths - Run only necessary workflows
- Parallel when possible - Maximize job concurrency
- Monitor cache hit rates - Optimize cache keys
- Set retention policies - Clean up old artifacts
Cost Optimization
# Only run expensive tests on main branch
jobs:
full-test-suite:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Run all tests
run: npm run test:all
smoke-tests:
if: github.ref != 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Run smoke tests
run: npm run test:smoke
Key Takeaways
✅ Cache dependencies to speed up workflows
✅ Use artifacts to share build outputs
✅ Implement concurrency control
✅ Filter paths to skip unnecessary runs
✅ Parallelize independent jobs
✅ Monitor and optimize cache effectiveness
✅ Set appropriate retention policies