Cutting Our CI/CD Build Time in Half
How we took a 12-minute CI/CD pipeline down to 6 minutes.
I'd Submit a PR and Go Make Coffee for 12 Minutes
PR goes up, 12 minutes until CI passes. On days with 10+ PRs, pure CI wait time alone exceeded 2 hours. With 5 team members, that's 10 hours of idle time per day across the whole team.
This wasn't coffee time -- it was a productivity problem.
Where Was the Time Going?
I broke down execution time for each GitHub Actions step. Checkout: 30s. Node.js setup: 20s. npm install: 3m 30s. Lint: 1m 30s. Type check: 2m. Tests: 3m. Build: 2m.
npm install was 30% of the total. And lint, type checking, and tests were running sequentially -- even though they could all run in parallel.
(To be precise, we started at 12 minutes 17 seconds.)
npm install Down to 15 Seconds
First thing was npm caching. Used actions/cache to cache node_modules, keyed on the package-lock.json hash. On cache hits, 3 minutes 30 seconds became 15 seconds.
Adding prefer-offline=true to .npmrc helped a bit too. Small, but these things add up.
Lint, Type Check, and Tests in Parallel
These three are independent tasks. Parallelized them using GitHub Actions' matrix strategy. Three jobs running simultaneously means total time equals the longest individual task.
The sequential segment that took 6 minutes 30 seconds dropped to 3 minutes. Each job had a small dependency install overhead, but thanks to caching, it was only about 30 seconds.
A README Edit Was Triggering the Entire CI
Added path filters so tests and builds only run when files under src/ change. Docs-only PRs now pass in 1 minute with just linting.
This one was honestly a bit embarrassing. We'd been waiting 12 minutes for a README typo fix.
Tests Got Trimmed Too
Leveraged Jest's --changedSince option to run only tests related to changed files. By diffing against the base branch per PR, average test time dropped from 3 minutes to 1 minute 30 seconds.
Full test suites now only run on merges to main.
Docker Layer Caching Was a Bonus
The deployment pipeline's Docker builds were also eating time. Used the cache-from option in docker/build-push-action to reuse layers from previous builds. Builds with no dependency changes went from 2 minutes to 30 seconds.
Multi-stage builds also shrank the final image from 800MB to 200MB. Nice bonus.
12 Minutes to 5 Minutes 40 Seconds
Across the team, that's 5 hours of saved wait time per day. 5 hours times 20 days times 12 months equals 1,200 hours. The work itself took two days.
CI/CD optimization is an investment that compounds daily. We're now looking into self-hosted runners too. GitHub Actions' default runners have 2 cores and 7GB -- more powerful machines could push build times even lower.
Slow CI forces context-switching, and that kills developer focus.