iTranslated by AI
3 Optimization Techniques to Reduce GitHub Actions CI/CD Time to 5 Minutes
Introduction
"CI/CD builds are too slow..."
"Tests take 30 minutes, so deploying is a pain..."
"Isn't it a waste of time to do a full build every time?"
When CI/CD pipeline execution time is long, development speed drops significantly. However, with proper optimization, it is possible to reduce build time from 30 minutes to 5 minutes (-83%).
This article explains three optimization techniques based on a hypothetical scenario. The numbers are estimates based on general benchmarks.
Assumed Scenario: Situation Before Optimization
First, let's look at the assumed CI/CD execution time before optimization.
Assumed Project
- Scale: Next.js 14 + TypeScript, iOS App (SwiftUI), 200,000 lines of code
- Team: Approximately 7 developers
- Deployment: Web (Vercel) + iOS (TestFlight)
Common CI/CD Problems
Execution time:
- Web CI: 15 min/run
- iOS CI: 30 min/run
- Daily CI runs: Average 20 times
- Total waiting time: Approx. 10 hours/day
Cost:
- GitHub Actions minutes: 20,000 min/month
- Overage charges: $400/month
- Developer waiting time cost: 1.5 million JPY/month
Process issues:
- No caching → Full build every time
- No parallelism → Tests executed serially
- Unnecessary steps → Deployment runs for every PR
A particularly problematic situation is that developers are waiting an average of 30 minutes × 20 times = 10 hours/day for CI to complete.
Expected Improvement Effects: -83% Reduction with 3 Optimizations
By properly implementing optimizations, you can expect the following improvements.
Improvement in Build Times
| Metrics | Before Optimization | After Optimization | Improvement Rate |
|---|---|---|---|
| Web CI Execution Time | 15 min | 3 min | -80% |
| iOS CI Execution Time | 30 min | 5 min | -83% |
| Total Daily Waiting Time | 10 hours | 2.5 hours | -75% |
| GitHub Actions Minutes | 20,000 min/month | 5,000 min/month | -75% |
| Monthly Cost | $400 | $0 | -100% |
Optimization Method 1: -60% Reduction with Dependency Caching
The most effective approach is dependency caching.
Before: No Caching (15 minutes)
# .github/workflows/test.yml (Before)
name: Test
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
# ❌ Full install every time: 8 minutes
- name: Install dependencies
run: npm ci
# Run tests: 7 minutes
- name: Run tests
run: npm test
Execution Time Breakdown:
-
npm ci: 8 min (Download dependencies + Installation) - Test execution: 7 min
- Total: 15 min
After: Caching Enabled (6 minutes)
# .github/workflows/test.yml (After)
name: Test
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
# ✅ Enable caching
cache: 'npm'
# ✅ On cache hit: 30 seconds
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Execution Time Breakdown (on cache hit):
-
npm ci: 30 sec (Restored from cache) - Test execution: 7 min
- Total: 7.5 min (-50%)
iOS: Caching CocoaPods
# .github/workflows/ios.yml
name: iOS CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
# ✅ CocoaPods cache
- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
# On cache hit: 1 min (Full install: 12 min)
- name: Install CocoaPods dependencies
run: |
if [ ! -d "Pods" ]; then
pod install
fi
# ✅ Build cache (DerivedData)
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-deriveddata-${{ hashFiles('**/*.swift') }}
- name: Build and test
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15'
Caching Effects:
- CocoaPods: 12 min → 1 min (-92%)
- Xcode DerivedData: 15 min → 3 min (-80%)
Optimization Method 2: -50% Reduction with Parallel Execution
Next, you can expect significant results from parallel execution of tests.
Before: Serial Execution (7 minutes)
# Before: Everything executed serially
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
# 1. Lint: 2 min
- run: npm run lint
# 2. Unit tests: 3 min
- run: npm run test:unit
# 3. E2E tests: 2 min
- run: npm run test:e2e
Total execution time: 7 minutes
After: Parallel Execution (3 minutes)
# After: Parallelizing jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: 'npm'
- run: npm ci
- run: npm run lint # 2 min
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: 'npm'
- run: npm ci
- run: npm run test:unit # 3 min
e2e-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: 'npm'
- run: npm ci
- run: npm run test:e2e # 2 min
By using parallel execution, it completes in 3 minutes, the duration of the longest job (-57%)
Further Parallelization with Matrix Strategy
# Run E2E tests in parallel across multiple browsers
jobs:
e2e-test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
shard: [1, 2, 3] # Split into 3
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
# Run split tests with sharding
- run: npx playwright test --shard=${{ matrix.shard }}/3
env:
BROWSER: ${{ matrix.browser }}
Effects:
- E2E tests: 6 min → 2 min (split into 3 shards)
- Multi-browser tests: Sequential execution → Parallel execution
Optimization Method 3: Reducing Unnecessary Steps with Conditional Execution
Finally, limiting execution based on changed files is also effective.
Before: Always executing all steps
# Before: Running deployment even on PRs (wasteful)
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: npm run build
- run: vercel --prod # Runs on PRs as well (unnecessary)
After: Conditional execution
# After: Deploying only on the main branch
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # ✅ main only
steps:
- run: npm run build
- run: vercel --prod
# Preview deployment only on PRs
preview:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- run: npm run build
- run: vercel # Preview environment
Execution control with file change detection
# Run Web tests only when Web files are changed
jobs:
web-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch history
# ✅ Detect changed files
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: |
src/**
package.json
package-lock.json
# Execute only if web files have changed
- name: Run web tests
if: steps.changed-files.outputs.any_changed == 'true'
run: npm run test:web
iOS: Speeding up code signing with Fastlane Match
# iOS: Code signing cache
jobs:
ios-build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
# ✅ Manage certificates with Fastlane Match
- name: Setup certificates
run: fastlane match development --readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
# ✅ Build only (Archive only on main)
- name: Build
if: github.event_name == 'pull_request'
run: xcodebuild build -workspace MyApp.xcworkspace -scheme MyApp
# Archive & Upload only on the main branch
- name: Archive and Upload
if: github.ref == 'refs/heads/main'
run: fastlane beta
Effects:
- iOS CI on PRs: 30 min → 5 min (Build only)
- Deployment on main: 30 min → 12 min (Leveraging cache)
Performance Comparison After Optimization
| Step | Before Optimization | After Optimization | Improvement Rate |
|---|---|---|---|
| Dependency Installation | 8 min | 0.5 min | -94% |
| Test Execution (Parallelization) | 7 min | 3 min | -57% |
| Conditional Execution | Deploy every time | Skip for PRs | -100% |
| Total (Web) | 15 min | 3 min | -80% |
| Total (iOS) | 30 min | 5 min | -83% |
Cost Reduction Effects
| Metrics | Before Optimization | After Optimization | Savings |
|---|---|---|---|
| GitHub Actions Minutes | 20,000 min/month | 5,000 min/month | - |
| Overage Charges | $400/month | $0/month | $4,800/year |
| Developer Waiting Time | 10 hours/day | 2.5 hours/day | 1.25 million JPY/month |
| Annual Savings | - | - | Approx. 15 million JPY |
Summary
| Optimization Method | Implementation Difficulty | Improvement Effect | Recommendation Level |
|---|---|---|---|
| Dependency Caching | ★☆☆ | -60% | ★★★ |
| Parallel Execution | ★★☆ | -50% | ★★★ |
| Conditional Execution | ★☆☆ | -30% | ★★☆ |
CI/CD optimization, when implemented correctly, will dramatically improve development speed. Let's start by enabling caching.
Discussion