Releases

Speed Up TypeScript Build Times (TS 5.7/5.8): Fix Your Graph

Speed Up TypeScript Build Times (TS 5.7/5.8): Fix Your Graph I’ve watched one bad root tsconfig.json turn a 12-second typecheck into a 6-minute slog. TypeScript usually isn’t “slow” by itself. Your repo layout makes it slow, by forcing the compiler to keep re-walking the same graph, re-resolving the same imports, and re-checking code that did […]

Jack Pauley December 31, 2025 6 min read
speed up TypeScript build

Speed Up TypeScript Build Times (TS 5.7/5.8): Fix Your Graph

I’ve watched one bad root tsconfig.json turn a 12-second typecheck into a 6-minute slog.

TypeScript usually isn’t “slow” by itself. Your repo layout makes it slow, by forcing the compiler to keep re-walking the same graph, re-resolving the same imports, and re-checking code that did not change.

Measure first, or you’ll “fix” the wrong thing

Start with numbers.

This bit me on a monorepo where everyone argued about compiler flags, and nobody noticed we included dist and a generated client folder in the program.

  • Cold typecheck baseline: Run tsc –noEmit and record wall time. Use /usr/bin/time -l on macOS, use /usr/bin/time -v on Linux.
  • Where the time goes: Run tsc –noEmit –extendedDiagnostics and look at Files, Check time, and resolveModuleName time.
  • If you already use references: Run tsc -b –noEmit –verbose and confirm it only rebuilds the project you touched plus its dependents.

Ignore the GitHub commit count. It’s a vanity metric.

What you want is boring: a small “Files” number, stable module resolution time, and no more “TypeScript: Restart TS server” rituals in VS Code.

Stop re-checking unchanged code: use project references and build mode

Here’s the thing.

If you keep running tsc -p against a mega-config, TypeScript has no choice but to drag half your repo back into memory and typecheck it again.

Do this instead:

  • Create a solution tsconfig at the repo root: Put “files”: [] and only references. Do not put a giant include here.
  • Make each package a real project: Add “composite”: true and “incremental”: true in each package tsconfig.
  • Run build mode everywhere: Use tsc -b in CI and locally. Once you adopt references, build mode stops being “optional.”

I don’t trust “known issues: none” from any project.

So when you flip to references, keep a rollback plan. Run both commands in CI for a day if you need to: one job runs your old tsc -p, another runs tsc -b, compare results, then delete the old one.

A fast reference layout you can copy

Keep the root config dumb.

Let it describe the graph, not the files.

  • Root tsconfig.json: references only. No include.
  • Package tsconfig.json: composite + incremental + a stable tsBuildInfoFile path.
  • Libraries emit types: use emitDeclarationOnly when a bundler or another step handles JavaScript.

Some folks skip references because it “adds config.” I get it.

But if your repo rebuilds 25,000 files after a one-line change, config is not your biggest problem.

Cut the program size: inclusion, boundaries, skipLibCheck

Small config wins.

Every extra file you include expands the program, expands the watch set, and drags module resolution along with it.

  • Over-inclusion fix: Exclude dist, build, generated clients, and test/story trees. If VS Code watches them, your laptop fans will tell you.
  • Declaration boundaries: Downstream projects should consume .d.ts outputs across project references. Do not keep importing other packages’ src through broad path aliases unless you enjoy rechecking everything.
  • skipLibCheck (use it on purpose): Turn it on for app projects when third-party type packages dominate your check time. Consider keeping it off for your “core” shared package if you treat it like an internal SDK.

Module resolution: pick a mode that matches reality

This is where teams get weird.

They pick nodenext because it sounds modern, then spend a sprint chasing ESM and conditional exports edge cases.

My rule is simple, and it saves fights:

  • Bundler-built web apps: start with moduleResolution: “bundler”.
  • Node services executed by Node: start with moduleResolution: “node16”.
  • Shipping an ESM package that must match Node’s rules: consider nodenext, but expect more friction and more resolution work in projects with heavy package.json exports.

If –extendedDiagnostics shows module resolution dominating, treat it like an import graph problem.

In practice, that means trimming path aliases, avoiding “match everything” patterns, and keeping boundaries clean so TypeScript does not keep peeking into other packages’ source trees.

CI and VS Code: cache .tsbuildinfo and keep the editor out of the blast radius

Cache the build info.

If CI throws away .tsbuildinfo every run, you pay cold-start prices forever.

  • Put tsbuildinfo in a stable cache path: set tsBuildInfoFile under something you already cache, like node_modules/.cache/tsbuildinfo.
  • Separate typecheck from emit: run tsc -b –noEmit for the fast “gate.” Run declaration emit only where you actually need it.
  • Watch at the solution level: use tsc -b -w. If CPU stays high with no edits, you are watching too much.
  • Fix the editor the same way you fix CI: reduce program size. Root solution config should not include the whole repo.

If VS Code feels stuck, open the TS Server log and look at what it’s loading. Guessing wastes afternoons.

Other stuff in this release: dependency bumps, some image updates, the usual.

Migration path that won’t blow up your repo

Go one package at a time.

Unless you love big-bang refactors, in which case, sure, yolo it on Friday.

  • Step 1: Run tsc –noEmit –extendedDiagnostics. Write down total time, files, and module resolution time.
  • Step 2: Create a root solution tsconfig with files: [] and references only. Make sure tsc -b –noEmit works from the root.
  • Step 3: Convert one shared library to composite and emit declarations. Update downstream projects to reference it.
  • Step 4: Normalize moduleResolution by project type. Do not mix runtime models in one config unless you enjoy confusing import behavior.
  • Step 5: Add CI caching for .tsbuildinfo and verify by running the same job twice. The second run should drop.

Bottom line

Fast TypeScript builds come from one move: make the compiler do less work per change.

Use project references plus tsc -b so unchanged projects do not get rechecked. Pick moduleResolution that matches your runtime. Cache .tsbuildinfo like you mean it. If the editor feels slow, it usually points at the same root cause: your program got too big because your boundaries got fuzzy.