GitHub Actions Performance Matrices: CI Gating & Budget Enforcement
Architecting deterministic CI gates requires isolating performance budgets across multiple execution environments. This workflow replaces aggregate scoring with strict per-axis threshold enforcement, ensuring that regressions on mobile 4G or low-end CPUs block merges before deployment.
Implementation Steps
- Map critical user journeys to matrix axes
- Establish baseline Lighthouse CI thresholds
- Configure PR status check routing
Configuration
# .github/workflows/perf-matrix.yml
name: Performance Matrix Gating
on: [pull_request]
jobs:
lighthouse-audit:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- device: desktop
network: 5g
viewport: 1920x1080
- device: mobile
network: 4g
viewport: 375x812
steps:
- uses: actions/checkout@v4
- name: Run Lighthouse CI
run: npx lhci autorun
// lighthouserc.json
{
"ci": {
"collect": {
"numberOfRuns": 3,
"settings": {
"preset": "desktop"
}
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.90 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
}
}
}
}
Defining Matrix Axes for Performance Budgets
Matrix expansion must be explicitly bounded to prevent combinatorial runner explosion. Define discrete axes using include arrays rather than Cartesian products to maintain predictable execution times. This targeted approach extends beyond baseline Lighthouse CI & WebPageTest Integration by enforcing strict pass/fail criteria per matrix variant rather than relying on aggregate scores.
Implementation Steps
- Create
includearrays mappingdevice,network, andthrottlevariables - Inject matrix variables into
lighthouserc.jsonvia environment variables - Validate budget JSON schema against Lighthouse CI parser
Configuration
# strategy.matrix.include binding
strategy:
matrix:
include:
- profile: "mobile-4g"
cpu_throttle: 4
network_throttle: "4G"
- profile: "desktop-fiber"
cpu_throttle: 1
network_throttle: "5G"
// budgets.json with per-axis overrides
{
"profiles": {
"mobile-4g": {
"first-contentful-paint": 1800,
"largest-contentful-paint": 3500
},
"desktop-fiber": {
"first-contentful-paint": 800,
"largest-contentful-paint": 1500
}
}
}
Runner Provisioning & Dependency Resolution
Matrix execution multiplies dependency installation overhead. Implement hash-based artifact storage to isolate node_modules and build outputs per axis. Proper cache warming reduces pipeline duration by up to 60%. For persistent budget file management and assertion routing across distributed runners, consult Lighthouse CI Configuration & Storage to standardize JSON payload handling. Teams should also review Setting Up GitHub Actions Caching for Faster CI to implement cross-job cache sharing and fallback strategies.
Implementation Steps
- Generate cache keys using
hashFiles('**/package-lock.json', '**/lighthouserc.json') - Configure
setup-nodewith explicitcachepaths - Implement fallback restore keys for partial cache hits
Configuration
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Cache Node Modules
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-perf-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-perf-
Parallel Execution & Test Sharding
Unbounded parallelism triggers runner starvation and inconsistent CPU throttling, which corrupts Lighthouse metrics. Implement max-parallel caps and isolate network-heavy matrix variants. Advanced sharding techniques for distributing URL lists across matrix axes are covered in Parallelizing Performance Tests in CI/CD.
Implementation Steps
- Set
max-parallellimits based on runner pool capacity - Define
concurrencygroups usinggithub.workflowandgithub.ref - Route heavy synthetic payloads to dedicated high-memory runners
Configuration
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lighthouse-audit:
strategy:
max-parallel: 4
matrix:
include: [...]
runs-on: ${{ matrix.profile == 'desktop-fiber' && 'ubuntu-22.04-8core' || 'ubuntu-latest' }}
Threshold Enforcement & PR Gating
Deterministic gating requires isolating network variability and standardizing CPU throttling. When routing matrix jobs through enterprise infrastructure, ensure consistent bandwidth caps by deploying isolated agents. Provisioning steps for dedicated synthetic workers are outlined in WebPageTest Private Instance Setup.
Implementation Steps
- Configure
lighthouse-ciassertion exit codes (errorvswarn) - Map matrix failures to required PR status checks
- Implement retry logic with exponential backoff for flaky network conditions
Configuration
// lighthouserc.json assertions with error thresholds
{
"ci": {
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"interactive": ["error", { "maxNumericValue": 4000 }],
"total-byte-weight": ["warn", { "maxNumericValue": 2500000 }]
}
}
}
}
# continue-on-error handling with artifact upload
- name: Run Audit
id: audit
continue-on-error: true
run: npx lhci autorun --upload.target=temporary-public-storage
- name: Upload Report on Failure
if: steps.audit.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: lhci-report-${{ matrix.profile }}
path: .lighthouseci/
Cost Control & Runner Allocation
Matrix expansion directly impacts compute billing. Cap concurrent runners using conditional if statements and archive historical payloads to reduce storage overhead. Enterprise teams should implement tiered runner allocation to balance coverage against spend, following the cost-control frameworks in Optimizing GitHub Actions Runner Costs.
Implementation Steps
- Implement conditional matrix execution on
mainvs PR branches - Use self-hosted runners for high-frequency matrix jobs
- Archive historical Lighthouse JSON to reduce GitHub storage overhead
Configuration
- name: Skip Full Matrix on Draft PRs
if: github.event.pull_request.draft == true
run: echo "Running minimal audit only"
- name: Route to Cost-Optimized Runner
runs-on: ${{ github.ref == 'refs/heads/main' && 'self-hosted-perf-pool' || 'ubuntu-latest' }}
Troubleshooting & Flakiness Mitigation
Matrix-specific failures often stem from runner drift, transient network spikes, or inconsistent CPU pinning. Extract raw Lighthouse JSON per matrix variant for diffing to isolate the exact regression vector. Enforce viewport and CPU pinning across all jobs to guarantee metric reproducibility.
Implementation Steps
- Extract raw Lighthouse JSON per matrix variant for diffing
- Implement viewport and CPU pinning to eliminate runner drift
- Configure automated retries with jitter to bypass transient network spikes
Configuration
# Extract and diff Lighthouse reports across matrix runs
mkdir -p reports
cp .lighthouseci/*.json reports/
lhci compare --base ./reports/baseline/ --compare ./reports/current/ --output ./diff-report.html
- name: Retry on Network Flakiness
uses: nick-fields/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: npx lhci autorun
retry_wait_seconds: 15
Conclude with a strict gating policy that prevents merge on any error threshold breach. By isolating variables, enforcing deterministic budgets, and routing failures directly to PR checks, performance regressions are blocked before reaching production.