How to Speed Up TypeScript Builds in Monorepos Using Project References
Surprising fact: teams that adopt TypeScript Project References and incremental builds commonly see warm build times drop by 4x–10x — and CI times fall by 30–70% when combined with caching. If your monorepo feels sluggish, this is the lever that pays back fastest.
TL;DR — Quick wins to speed up TypeScript build
Enable composite + incremental in package tsconfigs, create a root tsconfig.json that lists references, and run builds with tsc --build. Add CI caching for .tsbuildinfo and emit directories. Use project references for type-check and declaration emit while using esbuild or swc for fast dev transpiles.
Why TypeScript build times explode in monorepos
Monorepos concentrate code, dependencies and type-surface area. Common pain points:
- Every
tscrun type-checks the whole repo if not partitioned. - Cascading recompiles: a small change in a core package forces rebuilds downstream.
- CI cold builds recompile everything, increasing minutes billed (often tens of minutes).
Symptom: local watch latency > 2s for common edits, or PR build times ballooning to 10–30 minutes. In practice, teams often see 2–6x slower developer feedback loops compared to a referenced setup.
Understanding TypeScript Project References and composite projects
Project References let you break a monorepo into smaller TypeScript “projects.” Each project emits its own declaration files and a .tsbuildinfo. The TypeScript compiler then reasons about inter-project changes instead of retypechecking everything.
Key flags:
composite: true— required for referenced projects (enablesdeclarationoutput and build info).incremental: true— writes.tsbuildinfoto speed subsequent builds.tsc --build(ortsc -b) — orchestrates builds across references and skips unchanged projects.
How to restructure a monorepo: practical tsconfig layout
Minimal layout (packages/foo, packages/bar):
{
"files": [],
"references": [
{ "path": "packages/foo" },
{ "path": "packages/bar" }
]
}
In packages/foo/tsconfig.json:
{
"compilerOptions": {
"composite": true,
"incremental": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"module": "esnext",
"target": "es2020"
},
"include": ["src"]
}
Then run:
tsc --build --verbose
# For watch mode
tsc -b -w
Path mappings
If packages import each other via names, set paths in the root tsconfig or use your package manager's hoisting/symlink behavior. For pnpm, be mindful of symlink layout — add explicit path mappings if editors can't resolve types.
Integrating with bundlers and declaration files
Unique insight: use TypeScript project references for authoritative type-checking and declaration (.d.ts) generation, and use a fast bundler (esbuild/swc) for dev builds. This hybrid avoids double work — tsc does type/declaration work once, while esbuild performs near-instant transpiles.
- Emit declarations from referenced projects (
declaration: true). - Consume compiled outputs or
declarationMapin bundlers where possible. - For Next.js or webpack, configure module resolution to prefer package
typesfields pointing at emitted declarations.
CI strategies: caching, parallel builds, orchestration
Cache these artifacts in CI:
.tsbuildinfofiles (speed incremental compile)- dist/build outputs and declaration files
Recommended cache key pattern: ts-buildinfo-{{hash of lockfile}}-{{commit short}} but pair it with a secondary key that uses package.json hashes to allow safe reuse. Use remote cache (Nx Cloud, Turborepo remote cache) for large teams.
Measuring improvements & a real-world case study
Benchmark patterns to measure: cold build, warm incremental build, and watch latency. Example metrics to collect: wall time, CPU, and CI minutes billed.
Case study — FinTech startup (60 packages): After enabling project references, configuring incremental, and adding CI caching for .tsbuildinfo and dist artifacts, the team saw:
- Cold CI build time drop: 12m → 5m (58% reduction)
- Warm incremental build on PR: 90s → 18s (5x faster feedback)
- Developer edit→test cycle reduced from ~45s to ~8s on average
These gains freed ~20% of CI concurrency and reduced billable minutes substantially.
Troubleshooting common pitfalls
- Circular references: identify via
tsc --build --verboseand refactor to remove cycles. - Stale
.tsbuildinfo: delete when changing compilerOptions that affect emit. - pnpm symlink resolution oddities: add explicit
pathsor usepreserveSymlinkssettings in Node tooling.
Migration checklist (quick)
- Audit packages and dependency graph (tooling: dependency-cruiser).
- Add
composite&incrementalto each package tsconfig. - Create root
tsconfig.jsonwithreferences. - Run
tsc -b, fix errors, add path mappings. - Add CI cache steps for
.tsbuildinfoanddist. - Adopt hybrid strategy: tsc for types/declarations, esbuild for dev bundling.
Conclusion — next steps
Project References are a pragmatic, high-ROI way to speed up TypeScript builds in monorepos. They reduce redundant work, enable parallelism, and pair well with CI caching and fast bundlers. In my experience, combining references with incremental build caching is the single most effective step teams take to reduce PR feedback time.
Get the complete guide with templates and checklists in our digital product — includes automated scripts to generate references, CI cache key recipes for GitHub Actions/CircleCI/GitLab, and troubleshooting playbooks. Get the complete guide with templates and checklists in our digital product.MonoRepo Speed Blueprint
Further reading: API design best practices, debugging techniques, microservices architecture
Comments
Post a Comment