TypeScript Build Performance: Faster Compilation with References
Improve your TypeScript build performance using incremental builds and project references. Learn how to configure your tsconfig for faster compilation today.
Waiting for the TypeScript compiler to finish on a large codebase feels like watching paint dry. Last month, I was working on a monorepo that took about 45 seconds to recompile after a single file change—an eternity when you're trying to maintain flow.
If your build cycle is dragging, you’re likely fighting against the monolithic nature of a standard tsc configuration. By shifting to typescript build performance strategies like incremental builds and project references, you can cut that time down significantly.
The First Mistake: Ignoring incremental
Before overhauling your architecture, check the low-hanging fruit. If you aren't using tsconfig incremental mode, you're missing out on the easiest win in the compiler's toolkit.
When you enable incremental: true in your tsconfig.json, TypeScript saves information about the project graph from the last compilation to a .tsbuildinfo file. On subsequent runs, the compiler uses this file to determine which files actually need to be checked.
JSON{ "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./.tsbuildinfo" } }
In my experience, this alone can shave off roughly 30% of your build time for medium-sized projects. It doesn't change how your code works, only how the compiler tracks changes. It's essentially free performance.
Architecting for Scale with Project References
When your project hits a certain size, even incremental builds get slow because the compiler still has to parse the entire dependency graph. This is where typescript project references become the gold standard.
Project references allow you to split a single large tsconfig into multiple smaller ones. Each sub-project acts as an independent unit. If you change a file in shared-utils, TypeScript only needs to recompile that specific package and the files that directly depend on it, rather than the entire application.
Setting up the Structure
To get this working, you need a root tsconfig.json that references your sub-projects:
JSON{ "files": [], "references": [ { "path": "./packages/ui" }, { "path": "./packages/api" } ] }
Inside each sub-project (like packages/ui/tsconfig.json), you must enable composite: true. This is non-negotiable; it tells the compiler that this project is part of a larger, referenced system.
| Feature | Standard Build | Project References |
|---|---|---|
| Incremental Tracking | Global | Per-project |
| Dependency Analysis | Full tree | Isolated sub-graphs |
| Build Speed | Linear decay | Constant (mostly) |
| Complexity | Low | Moderate |
Debugging the Bottlenecks
Even with these optimizations, sometimes you hit a wall. If your build is still slow, you need to see exactly what the compiler is doing. Use the --generateTrace flag to output a JSON trace file that you can visualize in Chrome.
Bashtsc --project tsconfig.json --generateTrace trace-output
This will reveal if you have "hot" files that are causing massive type-checking overhead. Often, I find that a single poorly typed utility function is being imported everywhere, forcing the compiler to re-evaluate it constantly. If you're looking for broader ways to measure this, I've written about final project audit & optimization: achieving production readiness to help you baseline these metrics.
Why You Shouldn't Over-Optimize
I once spent three days breaking a project into too many tiny references. The overhead of managing all those tsconfig files and the inter-dependency issues actually made the developer experience worse.
Fast typescript compilation is a goal, but it shouldn't come at the cost of project maintainability. If you find yourself needing to coordinate complex build orders manually, you might be better off using a tool like Turborepo or Nx, which handle project references under the hood.
Don't treat these settings as "set and forget." As your codebase grows, revisit your tsconfig optimization settings every few months. Are you still using tsBuildInfoFile effectively? Are your project boundaries still logical?
Next time, I’d probably focus more on avoiding any types in my shared interfaces earlier in the process—the compiler works significantly faster when it doesn't have to resolve complex any-heavy type hierarchies. It's a messy process, but getting your build times under the 5-second mark is absolutely worth the effort.
Frequently Asked Questions
Does composite: true make my build slower?
It adds a small amount of overhead because the compiler must generate declaration files (.d.ts) for every project. However, the gain in incremental build speed far outweighs this cost in large projects.
Can I mix incremental builds and project references?
Yes, and you should. They are designed to work together. The incremental flag tracks the state of each referenced project, ensuring that only the changed parts of your monorepo are recompiled.
What is the best way to monitor build performance?
Use tsc --extendedDiagnostics to see a breakdown of time spent in parsing, binding, and checking. If the "checking" phase is the longest, look for complex generic types or deeply nested imports.