Optimizing Workflow Performance
1. Using Caching Effectively
Example: Caching Node.js Dependencies
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
| Option | Values | Description |
|---|---|---|
| path | ~/.npm |
Directory to cache |
| key | ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} |
Unique identifier for cache |
| restore-keys | ${{ runner.os }}-node- |
Alternative keys to restore from |
Note: The
hashFiles function ensures cache invalidation when dependencies change.Example: Caching with Multiple Paths
steps:
- name: Cache multiple paths
uses: actions/cache@v3
with:
path: |
~/.cache/yarn
~/.npm
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
Warning: Avoid caching paths that contain sensitive data or are OS-specific without proper key handling.
Best Practices for Caching
- Use
hashFilesto invalidate cache on dependency changes - Cache directories, not individual files
- Set
restore-keysfor fallback cache restoration - Avoid caching sensitive information
2. Parallelizing Jobs
Example: Running Jobs in Parallel
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
test:
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: echo "Deploying..."
| Job | Runner | Description |
|---|---|---|
| build | ubuntu-latest |
Checks out the repository code |
| test | ubuntu-latest |
Runs unit and integration tests |
| deploy | ubuntu-latest |
Deploys the application to production |
Note: Jobs run in parallel by default unless dependencies are specified using
needs.
Best Practices for Parallel Jobs
- Use
needsto define job dependencies when necessary - Ensure jobs are independent to maximize parallelism
- Use matrix strategies for testing across multiple environments
3. Reducing Checkout Depth
By default, GitHub Actions checks out your repository with a depth of 100 commits. For workflows that don't require the full history, reducing this depth can significantly speed up checkout times and reduce resource usage.
| Option | Values | Description |
|---|---|---|
| fetch-depth | 0, 1, 10, etc. |
Number of commits to fetch. Set to 0 to fetch all history. |
Note: Setting
fetch-depth to 0 will fetch the entire history, which may be necessary for some CI/CD operations but increases checkout time.
4. Using Sparse Checkout
Sparse checkout allows you to check out only a subset of files from a repository, which can significantly reduce the time and resources needed for workflows that don't require the entire repository.
| Option | Values | Description |
|---|---|---|
| sparse-checkout | true |
Enables sparse checkout in the workflow |
| sparse-checkout-directories | ["src/", "docs/"] |
List of directories to include in the sparse checkout |
Example: Setting up sparse checkout
steps:
- name: Sparse Checkout
uses: actions/checkout@v4
with:
sparse-checkout: true
sparse-checkout-directories:
- src/
- docs/
Note: Sparse checkout is supported in
actions/checkout version 3 and above.
5. Minimizing Docker Layers
| Layer | Description | Optimization Strategy |
|---|---|---|
| FROM | Base image for the Docker container | Use minimal base images like alpine or slim to reduce initial layer size |
| RUN | Commands executed during build | Combine multiple apt-get or yum commands into one layer using && to reduce number of layers |
| COPY / ADD | Files copied into the image | Use COPY instead of ADD when possible. Avoid copying unnecessary files or directories |
| CACHE | Build cache mechanism | Structure your Dockerfile to take advantage of layer caching by placing less frequently changing instructions earlier |
Example: Optimized Dockerfile Layering
FROM node:18-alpine
# Install dependencies first to leverage caching
COPY package*.json ./
RUN npm ci --only=production
# Copy application code after dependencies
COPY . .
# Expose port and run app
EXPOSE 3000
CMD ["npm", "start"]
Note: The
npm ci command ensures consistent dependency installation and is faster than npm install.
Warning: Avoid placing frequently changing instructions (like copying source code) before dependency installation to prevent unnecessary cache invalidation.
6. Using Matrix Efficiently
| Field | Description | Example |
|---|---|---|
| strategy.matrix | Defines the matrix of build configurations to run | |
| strategy.matrix.include | Adds additional configurations to the matrix | |
| strategy.matrix.exclude | Removes specific configurations from the matrix | |
Example: Efficient Matrix Usage
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Note: Using
strategy.matrix allows you to run your workflow across multiple configurations in parallel, which can significantly reduce total execution time.
Warning: Avoid creating too many matrix combinations as it can lead to high resource usage and longer overall workflow times.
Best Practices for Matrix Usage
- Limit the number of combinations to avoid resource exhaustion
- Use
strategy.matrix.excludeto remove problematic configurations - Combine
strategy.matrix.includewithstrategy.matrix.excludefor fine-grained control - Use
strategy.fail-fastto stop the job if any matrix job fails
7. Skipping CI
| Option | Values | Notes |
|---|---|---|
| github.event_name | push, pull_request |
Event that triggers the workflow |
| github.ref | refs/heads/main, refs/pull/123/merge |
Branch or tag reference |
| github.event.head_commit.message | Commit message content | Used to check for skip keywords |
Example: Skip CI with commit message
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
run: npm test
Note: The
[skip ci] keyword is case-insensitive and can appear anywhere in the commit message.
How It Works
- GitHub Actions evaluates the
ifcondition before running jobs. - If the commit message includes
[skip ci], the job is skipped entirely. - This is useful for documentation changes or other commits that don't require a full CI run.
8. Using Conditionals
| Expression | Description | Usage |
|---|---|---|
| github.event | Event data for the current workflow run | if: github.event_name == 'push' |
| github.ref | Branch or tag name that triggered the workflow | if: github.ref == 'refs/heads/main' |
| github.actor | Username of the person or app that triggered the workflow | if: github.actor == 'octocat' |
| github.workflow | Name of the workflow | if: github.workflow == 'Build and Test' |
| github.action | Name of the action being executed | if: github.action == 'setup-node' |
Example: Conditional Step Execution
steps:
- name: Run on Main Branch
if: github.ref == 'refs/heads/main'
run: echo "Running on main branch"
Example: Conditional on Event Type
if: github.event_name == 'pull_request'
run: echo "This runs only on pull requests"
Note: Use
if expressions to control when steps or jobs run based on context variables.
Warning: Avoid complex logic in
if conditions; keep them simple for better readability and maintainability.
9. Caching Docker Layers
| Option | Values | Notes |
|---|---|---|
| uses | actions/cache@v3 |
Cache action for GitHub Actions |
| key | docker-${{ hashFiles('Dockerfile') }}-layer-${{ steps.build.outputs.layer }} |
Unique cache key based on Dockerfile and layer |
| path | /root/.docker |
Path to Docker cache directory |
Example: Caching Docker layers in a workflow
name: Build with Docker layer caching
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Cache Docker layers
uses: actions/cache@v3
id: cache-docker
with:
path: /root/.docker
key: docker-${{ hashFiles('Dockerfile') }}-layer-${{ steps.build.outputs.layer }}
- name: Build Docker image
id: build
run: |
docker build -t my-app .
echo "layer=$(docker inspect --format='{{.Id}}' my-app)" >> $GITHUB_OUTPUT
10. Using Prebuilt Actions
| Property | Description | Usage |
|---|---|---|
| uses | Specifies the action to use from a repository | uses: actions/checkout@v4 |
| with | Provides input parameters to the action | with: token: ${{ secrets.GITHUB_TOKEN }} |
Example: Using a prebuilt action to checkout code
name: Checkout Code
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
Example: Using a prebuilt action with inputs
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy with GitHub Actions
uses: actions/deploy@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
environment: production
Note: Prebuilt actions are available in the
actions namespace and can be found on GitHub Marketplace or in public repositories.
Benefits of Using Prebuilt Actions
- Reduce workflow setup time by reusing existing actions
- Ensure consistency across workflows with standardized actions
- Access to community-maintained actions with frequent updates
11. Optimizing Runner Selection
| Runner Type | Description | Use Case | Notes |
|---|---|---|---|
| ubuntu-latest | Latest Ubuntu Linux runner | General-purpose workflows, Linux-specific builds | Includes latest tools and dependencies |
| macos-latest | Latest macOS runner | macOS-specific builds, Xcode projects | High memory and CPU resources |
| windows-latest | Latest Windows runner | Windows-specific builds, .NET projects | Supports PowerShell and CMD scripts |
| self-hosted | Custom runner instance | Specialized environments, secure builds | Requires setup and maintenance |
Example: Using specific runner
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
Note: Using
latest runners ensures you get the most up-to-date tools but may introduce breaking changes. Pin to specific versions for stability.
Best Practices
- Pin runner versions for stable builds
- Use
self-hostedrunners for sensitive or custom environments - Choose
ubuntu-latestfor most Linux-based workflows - Use
macos-latestorwindows-latestfor platform-specific builds
12. Monitoring Workflow Duration
| Metric | Description | Collection Method | Notes |
|---|---|---|---|
| workflow_duration | Total time taken for a workflow to complete | GitHub Actions UI or API | Includes setup, job execution, and cleanup times |
| job_duration | Time taken for an individual job to complete | GitHub Actions UI or API | Can be used to identify bottlenecks in specific jobs |
| step_duration | Time taken for each step within a job | GitHub Actions UI or API | Helps pinpoint slow steps in a job |
Example: Using GitHub Actions API to fetch workflow duration
const fetchWorkflowDuration = async () => {
const response = await fetch(
'https://api.github.com/repos/octocat/Hello-World/actions/workflows'
);
const workflows = await response.json();
console.log(workflows);
};
Note: Workflow duration data is available via the GitHub Actions API and can be used to optimize performance.
13. Using Workflow Templates
Example: Reusing a workflow template
name: Reuse Template
on:
push:
branches:
- main
jobs:
build:
uses: octocat/template-repo/.github/workflows/build.yml@v1
with:
node-version: "20"
| Property | Description | Values |
|---|---|---|
| uses | Path to the workflow template | owner/repo/.github/workflows/workflow.yml@ref |
| with | Input parameters to pass to the template | Key-value pairs |
Note: Templates must be stored in the
.github/workflows directory of a repository.
Warning: Template workflows are executed in the context of the repository where they are used, not the template's repository.