Why TypeScript 7's VS Code Benchmark Won't Predict Your CI Build Times

Why TypeScript 7's VS Code Benchmark Won't Predict Your CI Build Times

pr0h0••15 min read•coding
AI Usage (83%)
On this page

Introduction

A report attributed to tech-insider.org — dated 2026-11-03 in the feed where I found it — claims TypeScript 7 builds VS Code 11.9x faster than TypeScript 6. The framing: compiler performance is the headline theme of the next major release.

Where I come out up front: the number is almost certainly a real measurement of something, and a speedup that large has a plausible mechanism. But it's a compiler-throughput result on one codebase, and the material I have doesn't spell out the conditions. It is not a forecast for your CI pipeline. Plan a migration around "our builds get 10x faster" and you get to explain to your manager why the pipeline only got 40% faster.

This post separates what the report says from what it leaves out, walks through where a speedup of that size actually comes from architecturally, and builds the harness to answer the only question that matters: how much of your wall clock is tsc? By the end you'll have reproducible commands, a projection table for your own build times, and a checklist for deciding whether a TypeScript 7 migration is worth it in your repo.

What the TypeScript 7 Benchmark Report Claims, and What It Leaves Out

The single number — an 11.9x VS Code build

The report compares TypeScript 7 against TypeScript 6 and reports an 11.9x faster build of VS Code. That is the entirety of the technical claim I have: one multiplier, one codebase, two compiler generations. I haven't reproduced it and won't pretend otherwise.

What a benchmark like this needs before you can plan against it:

What you need to knowWhy it changes the answer
CPU model and core countA parallel compiler on 2 vCPUs is a different product than on 16
Exact compiler versions"TypeScript 6" and "TypeScript 7" are moving targets
The exact commandtsc -b on a project-reference graph ≠ tsc --noEmit on one config
Cache stateCold .tsbuildinfo vs warm changes the ratio dramatically
ScopeType-check only, or the entire VS Code product build including bundling?
Run count and varianceOne run is an anecdote; p50 and p95 are data

The feed material contains none of it. Treat 11.9x as a signal that the compiler got a lot faster, not as an input to a capacity plan.

Confirmed, inferred, and still unknown

Confirmed by the source: the report exists, it is attributed to tech-insider.org with the date above, and it states the 11.9x figure for a VS Code build comparing TypeScript 7 to TypeScript 6.

Confirmed by a primary source: the TypeScript team has publicly described a native port of the compiler, written in Go, aimed at roughly an order of magnitude in build and editor performance, with the VS Code codebase as its worked example. That is Anders Hejlsberg's March 2025 post, A 10x Faster TypeScript. Per that post, the native compiler lands at TypeScript 7, with a JavaScript-based 6.x line continuing alongside it for compatibility. Plans shift, so check the current release notes before you depend on the version numbering.

Inferred, not stated by the source: the 11.9x likely measures the native compiler building the program and type-checking the VS Code source tree, not the full product build, which includes extension bundling, webview builds, and other tooling. If that inference holds, the number is a compiler number wearing a build number's clothes.

Unknown: hardware, core count, compiler flags, cache state, run count, and which TypeScript 6 build formed the baseline. Unknown means unknown, not "probably the same as other benchmarks."

Where a Large Compiler Speedup Actually Comes From

The architectural shift behind the claim

Today's compiler is JavaScript on a single thread. Every cold process pays JIT warm-up; every run allocates an enormous graph of nodes, symbols, and types into one heap and collects garbage over it while checking. Those costs aren't incidental — on a codebase the size of VS Code they're a large share of total time.

The native port changes the cost model instead of micro-optimizing it. Go is compiled ahead of time, so there's no JIT warm-up. Goroutines share memory rather than serializing through a message channel, so per-file work — parsing, binding, emitting — can run in parallel across cores. The team's own framing includes memory use and editor responsiveness, not just throughput: a smaller, more predictable heap is what keeps the language service responsive on huge projects.

Which compiler phases get faster, and which barely move

A headline multiplier never transfers cleanly because not every phase parallelizes. What follows is my reasoning from how the compiler is structured, not a published phase breakdown — treat it as a model to test, not a fact:

PhaseParallelizes?Why
ParseYes, cleanlyPurely per-file, no cross-file state
BindYes, cleanlySymbol tables are per-file before merging
Module resolution / file I/OPartlyI/O-bound; concurrency helps, disk does not
CheckPoorlyAssignability, inference, and declaration merging are cross-file and order-sensitive
EmitYes, cleanlyPer-file output
Declaration emitPartlyType printing pulls on global state

Check time is usually the biggest line item on real projects, and it's the phase where parallelism buys the least. That's the structural reason an 11.9x compiler does not become an 11.9x build: the workload you care about most is the one that resists the optimization most.

💪

Run npx tsc -p tsconfig.json --extendedDiagnostics on your own repo. The Check time line against Total time tells you what fraction of your compile sits in the phase that parallelizes worst. That ratio predicts your realistic gain better than any external benchmark.

Amdahl's law inside your own pipeline

The arithmetic that decides your outcome is Amdahl's law. If tsc is a fraction p of your pipeline and gets s times faster, your pipeline speedup is:

speedup = 1 / ((1 - p) + p / s)

With s = 11.9, that gives you the table below. These are computed values, not measurements:

tsc share of pipeline (p)Projected pipeline speedupCeiling at infinite speedup
10%1.10x1.11x
25%1.30x1.33x
50%1.85x2.00x
75%3.19x4.00x
90%5.69x10.00x

Look at the last column. Even if the new compiler took zero seconds, a pipeline where type-checking is 25% of wall clock gets 1.33x faster overall. That's the real decision boundary — which is why the first move is not to upgrade. It is to measure.

Why Your CI Wall Clock Will Not Drop 11.9x

Cold caches, shared vCPUs, and container overhead

Your CI runner starts cold. No .tsbuildinfo, no page cache warmed by the previous run, no warm module graph. It's also usually a shared VM with 2–4 vCPUs, so the new parallel compiler has fewer cores to spread across than the laptop where the benchmark ran. On top of that, a container's overlay filesystem adds I/O latency that a per-file parallel design can amplify — many goroutines hitting slow storage at once isn't always faster than one thread hitting it in sequence.

Everything in CI that is not tsc

Here's a typical frontend CI job. The shares are illustrative — replace them with your own phase-timer output:

StageIllustrative shareMoves with a faster compiler?
Checkout5%No
npm ci25%No
Codegen (GraphQL, protobuf)8%No
Type-check22%Yes
Bundling20%No
Tests17%Partly — if your test runner transpiles via the compiler
Artifact upload3%No

Two rows change. Five don't. That table is the whole argument, and you can build your own version in about twenty minutes.

The difference between a warm editor and a cold runner

In the editor, the language service keeps a program in memory and patches it as you type. In CI, every run rebuilds the world from scratch. The --incremental and -b machinery exists to close that gap, but only if the build info survives between runs:

## Persist type-check state across CI runs.
## The key must include every input that affects the program,
## or you will reuse a stale graph and get wrong results.
- uses: actions/cache@v4
  with:
    path: "**/*.tsbuildinfo"
    key: tsc-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/tsconfig*.json', '**/*.ts', '**/*.tsx') }}

Look at the last two hash inputs. Leave source files out of the key and you hit the classic bug: a cache hit, a stale .tsbuildinfo, and a build that skips type-checking files you just changed. Include them and the cache will almost never hit — which tells you something honest. Cross-commit incremental builds are harder than they look.

Building a Timing Harness You Can Trust

Capture a baseline on your own hardware first

Pin the environment so the numbers mean something:

node -v && npm -v && npx tsc -v
lscpu | grep -E 'Model name|^CPU\(s\)'   # or: sysctl -n machdep.cpu.brand_string

Then measure the compiler alone, split into cold and warm, with hyperfine:

time-tsc.sh
#!/usr/bin/env bash
set -euo pipefail

RUNS=${RUNS:-7}

## COLD: delete every build-info file before each run
hyperfine --warmup 1 --runs "$RUNS" --prepare 'find . -name "*.tsbuildinfo" -not -path "*/node_modules/*" -delete' --export-json tsc-cold.json 'npx tsc -b'

## WARM: reuse whatever the previous run produced
hyperfine --warmup 1 --runs "$RUNS" --export-json tsc-warm.json 'npx tsc -b'

Run it twice — once on TypeScript 6, once on TypeScript 7 — with the same node_modules resolution, the same flags, nothing else changed. Change the compiler and the bundler in the same commit and you have measured nothing useful.

Isolating type-check time from install, bundle, and test

You need the phase split, not just the total. This wrapper times each stage and emits JSON you can diff across compiler versions:

phase-timer.mjs
import { spawnSync } from "node:child_process";


// Adjust the commands to your repo; keep them identical across runs.
const phases = [
["install",   "npm ci --prefer-offline --no-audit --no-fund"],
["typecheck", "npx tsc -b"],
["bundle",    "npx vite build"],
["test",      "npx vitest run --reporter=dot"],
];

const timings = {};

for (const [name, cmd] of phases) {
const start = performance.now();
const result = spawnSync(cmd, { shell: true, stdio: "inherit" });

if (result.status !== 0) {
  console.error("phase failed:", name, "exit", result.status);
  process.exit(result.status ?? 1);
}

timings[name] = Number(((performance.now() - start) / 1000).toFixed(2));
}

const total = Object.values(timings).reduce((a, b) => a + b, 0);
console.log(JSON.stringify({ timings, total, at: new Date().toISOString() }, null, 2));

The number you want is timings.typecheck / total. That ratio is p in the table above, and it's the only thing between the benchmark report and your reality.

For a finer split inside the type-checker itself, --extendedDiagnostics breaks the work into Parse time, Bind time, Check time, Emit time, and Total time, plus workload counters like Types and Instantiations. The counters matter more than the timings across compiler generations: millions of Instantiations means your bottleneck is generic inference in your own code, and no compiler upgrade fixes a type-level utility that expands exponentially.

Reporting p50 and p95 instead of one lucky run

A single CI number is a coin flip. Pull percentiles out of hyperfine's times array:

jq -r '
  .results[0] as $r
  | ($r.times | sort) as $t
  | ($t | length) as $n
  | "median=\($r.median)s",
    "p95=\($t[((($n - 1) * 0.95) | floor)])s",
    "max=\($r.max)s"
' tsc-cold.json

Report p50 for the typical developer experience and p95 for the CI you actually pay for. If p95 is 3x p50, your problem is runner variance, not the compiler — fix that before benchmarking anything else.

Editor Feedback Is a Separate Benchmark

Project load, check time, and the language service

Editor responsiveness is a different workload: incremental, warm, interactive. In VS Code, run TypeScript: Open TS Server Log and read the project-load and check entries. The format drifts between versions, so read the surrounding lines instead of pattern-matching one string. You want to know which dominates: project load (big include globs, a large node_modules type surface) or check time (actual type complexity).

A native compiler that removes JIT warm-up should help cold start the most — exactly the first-open-a-big-repo experience. That's an inference from the architecture, not something I measured.

Watch mode, incremental state, and .tsbuildinfo reuse

tsc -b --watch is where incremental state pays off and where it also bites. Two rules that keep you out of trouble:

  • Never share a .tsbuildinfo between different tsconfig files or different outDir settings. Set tsBuildInfoFile explicitly in composite projects.
  • If a build ever behaves as if a file didn't change, --force on tsc -b is the diagnostic, not the fix. A stale build-info file that outlives a config change is a correctness bug, not a performance feature.

Downstream tools that embed the compiler

This is the migration risk almost nobody budgets for. These tools don't shell out to tsc — they import the compiler and drive its API:

  • typescript-eslint — type-aware lint rules need a Program
  • ts-node and ts-jest — compile-on-require
  • ts-morph, custom ts.TransformerFactory transforms, api-extractor, fork-ts-checker-webpack-plugin

If the new compiler is a separate implementation, the JavaScript API surface those tools call becomes a compatibility boundary that has to be bridged. I don't know the state of that bridge for TypeScript 7. The public plan describes API compatibility work, but the details and completeness are something to verify against the release notes for the version you adopt — not to take from a blog post. Including this one.

Migration Risks Worth Budgeting For

Tools pinned to the compiler API surface

Audit before you upgrade, not after:

## Anything that pulls in the compiler as a library rather than a CLI
npm ls typescript
grep -rn "from \"typescript\"\|require(\"typescript\")" --include="*.ts" --include="*.js" \
  --exclude-dir=node_modules .

Every hit is a place to re-validate. Deep imports into typescript/lib/* are the highest-risk category because they aren't a supported API at all.

Config and flag drift between compiler generations

Major compiler generations deprecate flags, change defaults, and tighten checks. I'm deliberately not listing specific removals — I don't have a verified list for this release, and guessing at config migrations is how people lose a day. Use tsc --showConfig to dump the effective configuration under each compiler and diff them:

npx tsc --showConfig > config-ts6.json
npx tsc --showConfig > config-ts7.json
diff config-ts6.json config-ts7.json

That diff is your actual migration checklist, generated instead of assumed.

A staged rollout order that keeps main green

  1. Measure first. Run phase-timer on the current compiler. If typecheck / total is under ~0.25, stop — the migration isn't worth it yet.
  2. Branch, don't flip. Build a branch with the new compiler and run the full test suite plus a clean tsc -b.
  3. Dual-run in CI for a week. Keep both compilers running on every PR so drift shows up as a red job, not as a surprise at merge time:
## Substitute the real published versions you are comparing.
strategy:
  matrix:
    ts: ["<current>", "<next>"]
steps:
  - run: npm i -D typescript@${{ matrix.ts }}
  - run: npx tsc -b --force
  - run: npm test
  1. Flip the default, keep the old job non-blocking for another week so you can attribute failures.
  2. Delete the old job and record the harness output as the new baseline.

Where the Speedup Is Genuinely Worth the Migration

Large project-reference graphs and monorepos

The payoff concentrates where the compiler is already the bottleneck: many packages, long reference chains, a CI job that spends minutes in tsc -b. These are the repos where typecheck / total runs above 0.5 and the Amdahl table promises a real 1.8–3x on the whole pipeline. They're also the repos whose monorepo tooling touches the compiler API most deeply, so the speedup and the migration risk scale together. Uncomfortable, but honest.

The local inner loop versus the CI wall clock

The asymmetry that matters: locally you run the type-checker dozens of times a day and it runs warm. In CI it runs once, cold, on fewer cores. The expected outcome for most teams is a much better inner loop and a modestly better pipeline. That's still worth having — developer time is expensive — but it's a different pitch than "our builds get 10x faster," and it should be sold internally that way.

Practical Checklist Before You Upgrade

CheckCommandPass condition
Compiler share of pipelinephase-timer.mjstypecheck/total ≥ 0.25 to be worth the effort
Cold vs warm gaptime-tsc.shWarm times show whether CI caching will help
Check-phase dominancetsc --extendedDiagnosticsCheck time / Total time is your realistic ceiling
Hot type-level code--extendedDiagnostics → InstantiationsUnder ~1M for a large app; millions means fix your types
API consumersnpm ls typescript + grepEvery hit has an owner and a validation plan
Effective config difftsc --showConfig on bothDiff reviewed and understood
Runner variancehyperfine JSON p95 vs p50p95 < 2x p50, or fix the runners first

The report's 11.9x may well be an accurate measurement of a real improvement. It is not a property of your codebase, and it won't predict your build times. Your number lives in timings.typecheck / total, and you can have it by lunchtime.

Further Reading

Share this post

More posts

Loading comments…