codeintelligently
Back to posts
Developer Productivity

How to Reduce Build Times and Why It Matters

Vaibhav Verma
7 min read
build timesCI/CDdeveloper productivityTurborepoVitestbuild optimization

How to Reduce Build Times and Why It Matters

Our build took 14 minutes. Nobody complained because it had always been 14 minutes. We treated it like weather. Just something you deal with.

Then we cut it to 90 seconds and everything changed.

PR throughput went up 28%. Developers stopped batching changes into massive PRs (because the feedback loop was fast enough to ship small ones). Code review quality improved because PRs were smaller. The flaky test rate dropped because developers could actually re-run tests without losing their afternoon.

All from fixing something nobody thought was a problem.

The Hidden Cost of Slow Builds

Build time isn't just "time waiting." It's a chain reaction of behavioral changes that compound into massive productivity loss.

Context switching. When a build takes more than about 90 seconds, developers switch to something else. Slack, email, Twitter, another task. Research from Microsoft shows that recovering from a context switch takes 15-25 minutes. So a 5-minute build doesn't cost 5 minutes. It costs 20-30 minutes of productive focus.

PR bloat. When builds are slow, developers batch changes to avoid triggering the pipeline repeatedly. Instead of three focused PRs, they submit one monster PR with 800 lines changed. Reviewers skim instead of reading. Bugs sneak through. Merge conflicts multiply.

Reduced experimentation. Fast builds encourage experimentation. "Let me try this approach and see if it works." Slow builds discourage it. "I'll just go with the first approach because I don't want to wait another 14 minutes to validate the alternative." Over time, this leads to worse technical decisions.

Flaky test tolerance. When re-running tests is cheap (30 seconds), developers investigate failures immediately. When re-running tests costs 14 minutes, they hit retry and hope. Flaky tests accumulate. Trust in the test suite erodes. Eventually, developers stop writing tests because they view the suite as unreliable.

Measuring What Matters

Before optimizing, measure. You need three numbers:

  1. Cold build time: From clean state to running application. This is what new branches and CI experience.
  2. Incremental build time: After changing one file. This is what active development feels like.
  3. Test suite duration: Unit tests, integration tests, and e2e tests separately.

I use this classification:

Build Type Good Acceptable Problem
Incremental < 2s 2-10s > 10s
Cold < 30s 30s-3min > 3min
Unit tests < 30s 30s-3min > 3min
Integration < 3min 3-8min > 8min
Full CI < 5min 5-15min > 15min

If any of your numbers fall in the "Problem" column, you're losing significant productivity.

The Playbook: From 14 Minutes to 90 Seconds

Here's the exact sequence of optimizations we applied. Each one is independent, but they compound.

1. Add Build Caching (Savings: 40-60%)

This is the single biggest win for most projects. Tools like Turborepo, Nx, or Gradle's build cache store the results of previous builds and skip unchanged work.

For a TypeScript monorepo, Turborepo's remote cache was transformative:

json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    }
  }
}

When developer A builds package X and pushes the cache, developer B skips that build entirely. In a monorepo with 20+ packages, most builds become cache hits. Our cold build went from 14 minutes to 6 minutes just from this.

2. Parallelize Test Execution (Savings: 30-50%)

Most test suites run sequentially by default. Split them:

yaml
# GitHub Actions example
test:
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - run: npx vitest --shard=${{ matrix.shard }}/4

Four parallel shards cut our test time from 8 minutes to 2.5 minutes. The wall-clock time is bounded by the slowest shard, so balance your test distribution.

3. Use Incremental TypeScript Compilation

If you're running tsc on every build, you're recompiling unchanged files. Enable incremental mode:

json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo"
  }
}

This alone cut our type-checking step from 45 seconds to 3 seconds for typical changes.

4. Optimize Docker Builds with Layer Caching

Docker builds are often slow because they invalidate the cache unnecessarily. Order your Dockerfile from least-changing to most-changing:

dockerfile
# Good: dependencies change less often than source code
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Use BuildKit's cache mounts for package managers:

dockerfile
RUN --mount=type=cache,target=/root/.npm npm ci

5. Replace Heavy Tools with Faster Alternatives

Sometimes the biggest win is swapping tools:

  • Jest to Vitest: Vitest is 3-5x faster for most projects because it uses Vite's native ESM support instead of Babel transforms.
  • Webpack to Vite/Turbopack: Modern bundlers are dramatically faster for development builds.
  • ESLint to Biome: Biome is 10-30x faster for linting and formatting. It's written in Rust and does both jobs in a single pass.

We replaced Jest with Vitest and our unit test suite dropped from 90 seconds to 18 seconds. No test changes required.

The Contrarian Take: Don't Optimize the Wrong Build

I see teams obsess over CI build times while ignoring local development speed. That's backwards. Developers run local builds 10-50x more often than CI runs. A 30-second improvement in local incremental build time is worth more than a 5-minute improvement in CI.

The priority should be:

  1. Local incremental build (developers feel this hundreds of times per day)
  2. Local test execution (developers run this dozens of times per day)
  3. CI pipeline (developers wait for this a few times per day)
  4. Cold build / environment setup (developers hit this a few times per week)

Optimize in that order. Most teams start at the bottom and work up, which is exactly wrong.

The Stealable Framework: The Build Time Budget

Set a build time budget for your team. Make it explicit and visible. Here's how:

  1. Set targets. Local incremental: under 2 seconds. CI: under 10 minutes. Cold: under 3 minutes.
  2. Monitor weekly. Add build time to your CI and track the p50, p90, and p99 over time.
  3. Create an alert. When the p90 exceeds your target by 20%, file a ticket automatically.
  4. Dedicate capacity. Reserve 10% of sprint capacity for build performance. This isn't overhead. It's investment in every other task moving faster.

The teams that treat build time as a first-class metric are, in my experience, the teams that ship the fastest. Not because fast builds are the most important thing. But because caring about builds means caring about the developer experience. And that mindset permeates everything.

Start measuring. Start caring. The 14-minute build you're tolerating right now is costing you far more than you think.

$ ls ./related

Explore by topic