Your application’s CI keeps getting slower over time. A little more every month, until the day it stops being an annoyance and turns into a blocker.

What’s your first move?

I watched this happen last week.

A Node + TypeScript service had a 15-minute CI. Unit tests with coverage, around 1,200 of them.

And the reasoning that shows up in the moment is always the same: if it’s slow, something is wrong, so it must be possible to optimize. Upgrade TypeScript to the Go version, which typechecks much faster. Swap eslint and prettier for ox, which are much faster.

None of that is false. TypeScript in Go is absurdly faster. Oxlint is faster than eslint.

And it still wasn’t going to fix anything.

TypeScript 7 really is that fast

The tsc you know was a TypeScript compiler written in TypeScript, running on Node. Version 7 is a faithful port of it to Go, shipped as a native binary, with type-checking parallelized across 4 threads.

Microsoft’s numbers are not modest: VS Code went from 125.7s to 10.6s, Sentry from 139.8s to 15.7s. In the editor, opening a file with errors dropped from ~17.5s to under 1.3s.

The emitted JavaScript is the same. The type system is the same. This is a compile-time story, full stop.

One detail if you plan to migrate: 7.0 has no programmatic API, and it only arrives in 7.1. Any tool that embeds the compiler - typescript-eslint, Volar, and by extension Vue, Svelte, Astro, Angular - stays on TypeScript 6.

The Japanese knife and the pot of beans

Amdahl’s Law explains why swapping the compiler was never going to move the needle:

Amdahl's Law: S equals 1 divided by ((1 - p) + p / s). Two bars of equal length compare before and after: in BEFORE, a narrow slice p and a very wide slice 1 - p; in AFTER, the p slice is a hair-thin sliver and the 1 - p slice is unchanged. As s grows, S approaches 1 / (1 - p).
The slice you optimized shrinks to a sliver. The other one doesn't move - and it's the one setting the total.

p is the fraction of the work you improved, s is how much you improved it. The painful part is the limit: as s approaches infinity, the maximum speedup of the whole system becomes 1 / (1 - p).

In other words: the part you didn’t optimize is your ceiling. And it doesn’t move.

Think about Sunday lunch. It takes you 40 minutes. You buy an expensive Japanese knife that chops onions 10x faster, and the marketing wasn’t lying. Except chopping onions took 2 minutes, and now it takes 12 seconds.

Lunch still takes a little over 38 minutes.

Because what takes time is the pot of beans on the stove. And beans are not a chopping problem, they are a waiting problem. No knife solves waiting.

They did the upgrade

Everything went up at once: eslint 8.57 -> oxlint 1.76, prettier 3.0 -> oxfmt 0.61, TypeScript 5.9 -> TypeScript 7.

step before after
lint 5.56s 3.73s
format:check 6.51s 3.42s
typecheck 4.46s 4.04s
typecheck for tests 4.50s 3.93s
total 21.03s 15.12s

The new engines delivered the promised 6x to 10x. tsc 7 compiles the entire project in 1.22s. What eats the difference is the fixed overhead of each invocation: spinning up the process and loading the tool costs more than the actual work.

Net result: 5.91 seconds. Out of 900.

And note that those four steps together were 21s in a 900s pipeline, which puts p at 2.3%. If the tools were infinitely fast, zero cost:

S equals 1 divided by (1 - 0.023) equals 1.024x. A long bar labeled 900s pipeline with a hair-thin red sliver at its left end, marked as 21s of lint, format, typecheck (p = 2.3%). Below it, a bar of almost the same length: 879s in the best possible case. Net gain: 21 seconds.
With infinitely fast tooling at zero cost, the 900s pipeline drops to 879s. That's the ceiling.

In the impossible scenario, CI drops from 15m00s to 14m39s. That was the ceiling, and it was available before a single line of code was written, from one thirty-second division.

Microsoft’s numbers aren’t a lie, either. Slack cut CI type-checking from 7.5 minutes to 1.25 minutes, but there it’s a dedicated tsc step, where the compiler is 100% of the work. Same compiler, same engine gain, opposite outcome: the difference isn’t in the tool, it’s in the denominator.

Measuring costs five minutes

GitHub Actions already shows the duration of every step in the UI, for free. Open the last run and read it before forming any theory. Then divide the step’s time by the total: that is the maximum you can gain by attacking it. If it comes out to 2%, you just saved yourself a week.

If you need to dig deeper, the tools already exist and are criminally underused:

  • jest --verbose gives you the duration per file. Three files usually account for half the time.
  • jest --detectOpenHandles finds promises that never resolve and timers that never clear, which leave the runner sitting around waiting for the event loop to drain after the tests already finished. It has existed for years.

Knuth wrote this in 1974, on the same page of Structured Programming with go to Statements that gave us the “premature optimization is the root of all evil” everyone quotes at half length:

“It is often a mistake to make a priori judgments about what parts of a program are really critical, since the universal experience of programmers who have been using measurement tools has been that their intuitive guesses fail.”

The problem is the jump

None of this means “don’t upgrade.” Do upgrade - TypeScript 7 is an impressive piece of engineering and I plan to migrate everything I can. Those 6 seconds don’t move CI, but they do move the agent loop, which runs typecheck and lint dozens of times an hour. That is a real gain, just in a different number.

What’s wrong is something else: going from “it’s slow” straight to “swap it for something faster,” without the thirty-second division in between. Tool speed is not system speed.

And swapping tools is comfortable because it looks productive: there’s a PR, there’s a changelog, there’s a nice benchmark to show. Measuring first looks like bureaucracy.

But measuring is what separates engineering from cheering.

Thanks for reading!