
Our build took nine minutes. Everyone hated it, and everyone had quietly accepted it.
Nine minutes doesn't sound like much until you do the math. Fifteen builds a day across the team, every day, for months. That's not a build time. That's a part-time job nobody's doing anything else with.
So one slow Thursday I decided to actually fix it. Not guess — measure. Four hours later the build ran in just over four minutes, and the fix was somewhere nobody had thought to look.
I didn't optimize anything for the first three hours. I profiled.
The single biggest lesson: your build is slow somewhere specific, and it's almost never where you assume. Profile first. Optimize the one thing the data points at. Measure again. Repeat.
In our case the villain wasn't compilation. It was the test step re-installing dependencies from scratch on every run because of a broken cache key. One line.
Photo by Luke Chesser on Unsplash
When a build is slow, the instinct is to start optimizing the thing you assume is slow. For most teams that's "compilation," so people reach for faster compilers, more parallelism, a beefier CI runner.
We almost did exactly that. Someone had already filed a ticket to upgrade the CI machines. It would have cost real money and saved maybe thirty seconds, because compilation was never the problem.
Optimizing without profiling is just expensive guessing. You can pour a week into the wrong 5% and feel productive the whole time. Google's web.dev performance guidance hammers the same point about page speed: measure first, because the bottleneck is almost never where intuition puts it. It's the exact discipline behind the debugging method that changed how I work — locate, don't flail.
Before changing anything, I made the build report its own timing. Almost every CI system and build tool can do this — you just have to ask.
The cheapest version is timestamps around each phase:
start=$(date +%s)
npm run lint
echo "lint took $(($(date +%s) - start))s"
The better version is whatever native profiling your tool offers — a build profile, a trace, a timing report. I dumped each phase's duration into a table and stared at it.
| Phase | Time |
|---|---|
| Checkout | 8s |
| Dependency install | 3m 40s |
| Lint | 25s |
| Compile | 1m 10s |
| Test | 3m 30s |
| Package | 50s |
The instant that table existed, the answer was obvious. We weren't compiling slowly. We were installing dependencies twice — once in the main job, once inside the test job — and both were missing the cache.
Here's the discipline that matters: fix the single biggest item, then re-measure. Don't fix five things at once. If you change five things and the build gets faster, you don't know which one helped, and you've probably wasted effort on four.
The dependency install was 3m40s and the test job paid that cost a second time. The cause was embarrassing: the cache key included a timestamp, so it never hit. Every build started from zero.
The fix was changing the cache key to hash the lockfile instead:
key: deps-${{ hashFiles('package-lock.json') }}
Now the cache only invalidates when dependencies actually change. Install dropped from 3m40s to about 15 seconds on a hit. That one change did most of the work.
The biggest performance wins are almost never clever. They're usually something dumb that everyone had stopped seeing.
Re-measuring showed the test job still reinstalling its own dependencies. The two jobs weren't sharing the cache. I made them share it, and the second install vanished too.
Then I looked at the test step itself. Three and a half minutes for our suite was high. The profile showed the tests ran serially. The runner had four cores doing nothing. One flag turned on parallel test execution and the suite finished in just over a minute.
Photo by Alexandre Debiève on Unsplash
Here's the whole sequence, because the order is the actual lesson:
That last step matters too. There's always another optimization. Knowing when to stop is part of the skill. The fourth-biggest bottleneck is real, but the time you'd spend chasing it is usually better spent shipping features. Optimization has diminishing returns, and the discipline of declaring "fast enough" and walking away is what separates a focused afternoon from a week-long rabbit hole. I shaved off the two big wins, glanced at the next bar on the chart, decided it wasn't worth a single additional hour, and closed the laptop. Done is a feature too. The build that had haunted the team for months was fixed in an afternoon, and the fix held because I'd attacked causes instead of symptoms. A profiling-first approach doesn't just find the bottleneck faster — it finds the right bottleneck, so your fix actually sticks instead of moving the problem somewhere you'll have to chase again next month.
The profiling-first approach is a checklist I now run everywhere:
It's worth sitting with the real price of a slow build, because it's almost always underestimated, and the underestimate is why teams tolerate it for months.
The obvious cost is the wall-clock minutes. Nine minutes times fifteen builds times a team — that's the number people quote. But the wall-clock time isn't even the expensive part. The expensive part is what a slow build does to how developers behave.
When a build takes nine minutes, you don't sit and watch it. You context-switch. You open Slack, you start another task, you lose the thread of what you were doing. By the time the build finishes, your head is somewhere else and you pay the re-loading tax to get back. A slow build doesn't cost you nine minutes; it costs you nine minutes plus the focus tax on both ends, every single time.
Worse, slow builds change what people are willing to do. When verifying a change is cheap, developers test small and often. When it's expensive, they batch — they make five changes before running anything, because running is painful. Batched changes are harder to debug, because when the build finally breaks you've got five suspects instead of one. The slow build silently pushes the whole team toward worse habits. So when I cut that build in half, the real win wasn't the minutes on a dashboard. It was the team going back to testing small, staying in flow, and trusting the pipeline enough to lean on it. Fast feedback isn't a luxury. It's the thing that keeps good engineering habits affordable — and protecting it is part of the same fight against the quiet frictions that burn developers out without anyone noticing.
Q: What if I don't have a profiler?
You have date and timestamps. Wrap each phase, print the duration. Crude beats blind. A rough table is enough to find the biggest bar.
Q: My build is mostly compilation — what then? Then the data has told you something real, and you can attack it with confidence: incremental builds, caching artifacts, parallelism, or splitting the build. The point is you know, instead of guessing.
Q: How often should I re-profile? Whenever the build creeps back up, and after any major dependency or pipeline change. Build performance rots silently; a new bottleneck appears the moment you fix the old one.
Q: Is faster CI really worth an afternoon? Do the math on builds-per-day across your whole team and multiply by the days left in the year. It's one of the highest-leverage afternoons you'll spend.
If profiling-before-fixing is a habit you want to build, try it on the next slow thing you own this week — and stick around, because most of what I write comes back to measuring instead of guessing.
We'd lived with a nine-minute build for months because it felt like a fact of nature. It wasn't. It was a broken cache key hiding behind everyone's assumptions.
The fix wasn't speed — it was looking. Three hours of measuring beat any amount of clever optimizing, because it pointed me at the one change that mattered instead of the four that didn't.
Next time something is slow, don't reach for a fix. Reach for a stopwatch first. What's the biggest bar on your chart — and are you sure, or are you guessing?
No following, no network, no luck. Just an unglamorous system I ran for eighteen months. Here's exactly what I did.

I went from 200 to 11,000 subscribers without hiring anyone. AI didn't write my newsletter — it did everything around it.

I chased big, audacious goals for years and burned out every time. Then I built my whole life around wins so small they felt like cheating.

Comments
Sign in to join the conversation
No comments yet. Be the first to share your thoughts!