Slash CI Time: Practical Guide to TypeScript Incremental Builds & Caching for Large Monorepos
Did you know teams adopting remote caches and incremental TypeScript builds often cut CI build time by an order of magnitude? In practice you can expect 60–90% faster incremental runs, and many orgs report up to 10x reduction in pipeline time after combining tsc project references with a remote cache. This guide gives a concise, practical overview so you can start shaving minutes off every developer loop and CI run.
Why incremental builds matter in large TypeScript monorepos
Large monorepos (dozens to thousands of packages) suffer from two related pain points:
- Long local rebuilds harm developer velocity — a cold full build that takes 20–40 minutes kills context switching.
- CI cost and queue time explode: every PR can trigger full rebuilds that waste minutes and dollars across the team.
Incremental builds (and caching) address both by reusing previous artifacts and by only rebuilding affected projects. Use the phrase TypeScript incremental build when designing the plan; it’s the technical lever that makes selective rebuilds possible.
Overview of strategies
There are four complementary levers:
- tsc project references — splits a repo into composable TypeScript projects with
--buildsupport and produces.tsbuildinfoartifacts. - Build artifacts (dist,
.tsbuildinfo) — treat them as cacheable outputs and persist them across runs. - Local + remote cache — local cache for dev speed, remote shared cache for CI and cross-machine reuse (Turborepo remote cache, Nx, or custom S3/GCS-backed store).
- Task runners / bundlers — Turborepo, pnpm workspace scripts, esbuild/tsup for fast outputs and well-defined outputs for caching.
Step-by-step: enable tsc project references and tsbuildinfo
At minimum you need each package with a tsconfig.json marked as composite and a root tsconfig.json listing references. Example:
{
"compilerOptions": {
"composite": true,
"outDir": "dist",
"incremental": true
}
}
Root tsconfig.build.json with references:
{
"files": [],
"references": [
{ "path": "packages/pkg-a" },
{ "path": "packages/pkg-b" }
]
}
Build with:
pnpm -w build
# or
tsc --build
# tsc will generate .tsbuildinfo files next to project outputs
Make .tsbuildinfo an artifact you persist to the cache; it encodes incremental state for the TypeScript compiler.
Tooling integration: Turborepo, pnpm, esbuild/tsup
Key integration points:
- Declare deterministic outputs. For Turborepo, include build outputs and tsbuildinfo in
turbo.jsonso the task runner knows what to cache:
{
"pipeline": {
"build": {
"outputs": ["dist/**", "*.tsbuildinfo"]
}
}
}
- pnpm: use a workspace and pin
pnpm-lock.yaml. In CI cache the pnpm store to avoid reinstall cold-starts (~/.pnpm-store), and include the lockfile checksum in cache keys. - esbuild/tsup: produce deterministic artifacts (strip timestamps, embed version/hash in filenames) so cache keys are stable.
CI best practices: preserving caches & cache keys
Design cache keys that change only when relevant inputs change. Good components for a cache key:
- Workspace name or package path
- Checksum of the package’s
tsconfig.jsonand source files (or a commit range) - Lockfile checksum (
pnpm-lock.yaml) and Node version
Example conceptual key:
cache-key: pkg-a-{{ checksum "packages/pkg-a/tsconfig.json" }}-{{ checksum "packages/pkg-a/src/**" }}-{{ checksum "pnpm-lock.yaml" }}
Preserve: dist/, *.tsbuildinfo, pnpm store. Use remote cache services (Turborepo Cloud, Nx Cloud, or S3/GCS-backed caches) to share build artifacts across all runners and developers—this is often where teams see the largest ROI.
Measuring impact & troubleshooting
Measure cold vs warm builds and cache hit rate. Minimal benchmark matrix:
- Cold CI build (empty cache)
- Warm CI build (cache primed)
- Local dev incremental (change single file)
Common pitfalls and fixes:
- Stale
.tsbuildinfomismatches: ensureincrementalandcompositesettings are consistent; include tsbuildinfo in the cache. - Non-deterministic outputs: remove timestamps or embed content hashes in assets.
- Cache thrash: split cache namespaces by team/area or shard large caches to avoid eviction storms.
Quick troubleshooting commands
# show build order and affected projects
tsc -b --verbose
# force full rebuild
tsc -b --clean && tsc -b
Real-world example: Acme Corp (pseudonym)
- Cold CI: 12 → 10 minutes (first-run improvement from better bundler configs)
- Warm CI (cache hit): 40 → 4 minutes — a 90% reduction and ~10x speedup on many PRs
Key changes: they treated .tsbuildinfo and dist/ as primary cache artifacts, used content-hash cache keys, and added a lightweight pre-warm step in their GitHub Actions workflow to prime the cache.
Realistic migration checklist (high-level)
- Identify high-cost packages (top N by build time)
- Add
composite: trueand references gradually per package - Configure Turborepo/pipeline outputs and add
.tsbuildinfoto cache - Implement CI cache keys (lockfile + tsconfig checksums)
- Measure cold vs warm, tune outputs, enable remote cache
Conclusion
If you run a large monorepo, combining TypeScript incremental build techniques (project references + .tsbuildinfo) with a robust remote caching strategy (Turborepo TypeScript cache or pnpm build cache patterns) is the single most impactful optimization you can make for developer velocity and CI cost. Unique insight: treat .tsbuildinfo as a first-class artifact — version it in your cache key, monitor its hit rate, and you'll avoid many silent invalidations that waste time.
Get the complete migration playbook, CI templates, cache-key patterns and a runnable sample repo in our digital product. Get the complete guide with templates and checklists in our digital product.
Next steps: try enabling project references on one package, add its .tsbuildinfo to your CI cache, and measure the first warm build. For deep dives see API design best practices, debugging techniques, and microservices architecture.




Comments
Post a Comment