The testing-gates lesson covered ordering and parallelizing tests to fail fast. This lesson is the other half of pipeline speed — not re-doing work that hasn't actually changed at all, since a slow pipeline erodes the exact "integrate continuously, cheaply" value this whole domain opened with.
3 min read
A CI run that takes 15 minutes instead of 2 doesn't just cost time — it changes behavior. Developers batch up more changes before pushing (since each push means a long wait), context-switch away and lose track of a failure, or start treating a red pipeline as background noise rather than something to fix immediately. Fast feedback is what makes "integrate continuously" (this domain's opening lesson) actually practiced, not just theoretically true.
Installing dependencies from scratch on every single run — npm ci
downloading every package fresh — is usually the single biggest
avoidable cost in a pipeline, since the dependency tree rarely changes
between consecutive commits:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-
- run: npm ciThe cache key is derived from the lockfile's hash — if package-lock.json
hasn't changed, the exact same cache hits and npm ci resolves everything
from the cache instead of the network. If the lockfile has changed, the
key misses, but restore-keys still pulls the closest previous cache as a
starting point rather than starting completely cold, which is faster than
no cache at all even on a lockfile change.
The Docker & Containers domain covers layer caching within one machine's local builds — but a CI runner is typically a fresh, ephemeral machine every time, with no local cache to reuse by default. BuildKit's cache export/import solves this by persisting layers somewhere the next CI run can pull from:
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: my-app:${{ github.sha }}
cache-from: type=registry,ref=my-app:buildcache
cache-to: type=registry,ref=my-app:buildcache,mode=maxWithout this, every CI-built image effectively rebuilds every layer from scratch on every run, regardless of how carefully the Dockerfile itself orders instructions for local caching — the caching strategy from Dockerfile best practices only pays off if something actually persists the cache between runs, and on ephemeral CI infrastructure, that's this mechanism, not local disk.
The testing-gates lesson covered running independent jobs concurrently (lint, typecheck, unit tests). A build matrix parallelizes the same job across multiple variations automatically:
jobs:
test:
strategy:
matrix:
node-version: [18, 20, 22]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm testThis runs the identical test job three times, once per Node version, in parallel rather than sequentially — genuinely necessary for a library that has to support multiple runtime versions, and a common way to shard a single very large test suite across several parallel workers purely for speed, independent of any actual version differences.
A cache keyed too broadly (or invalidated too rarely) can serve outdated dependencies or a stale build artifact that no longer matches the actual source — the same "works in CI, fails when deployed" mismatch the common-bugs lesson names as its first bug, just introduced by an overly aggressive cache instead of a rebuild-at-deploy-time step. The fix is the same principle as any cache: key it precisely on whatever actually determines correctness (the lockfile hash, the Dockerfile's content hash), never on something broader like "always cache, restore unconditionally," which trades correctness for speed in a way that eventually produces a confusing, hard-to-reproduce failure.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is dependency caching keyed on a lockfile's hash effective for speeding up CI?
2. Why does a Dockerfile's careful layer ordering alone not speed up builds running on CI infrastructure?
3. What does a build matrix accomplish that simple job parallelization doesn't?
4. What real risk does an overly broad or aggressive cache key introduce?
CI/CD & Deployment Pipelines