Get the next live webinar in your inbox

One email a month: the upcoming live event + free recording access for subscribers. No spam, unsubscribe anytime.

Lesson 10 of 10 ~12 min
Course progress
0%

Caching & Optimization Strategies

Speed up workflows with caching, artifacts, and performance optimization techniques

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

  1. Always cache dependencies - Speeds up by 50-80%
  2. Use artifacts for build outputs - Share between jobs
  3. Implement concurrency control - Cancel outdated runs
  4. Filter paths - Run only necessary workflows
  5. Parallel when possible - Maximize job concurrency
  6. Monitor cache hit rates - Optimize cache keys
  7. 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