Skip to Content
Course content

121: Build Caching Strategies for TypeScript Monorepos

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, I've seen this a dozen times in growing engineering teams: the build times start creeping up, the CI pipeline becomes a bottleneck, and someone suggests, "We just need to turn on incremental: true in our tsconfig.json." They think that because TypeScript can remember what it did last time, the monorepo problem is solved. It isn't.

Thinking incremental: true is a Silver Bullet

Here is why that line of thinking fails you in a real monorepo. The incremental flag creates a .tsbuildinfo file. This file is great for your local machine because it tells the compiler, "Hey, I already checked this file, and nothing changed." But in a modern CI/CD environment, every build usually starts on a fresh container. That .tsbuildinfo file vanishes. You're essentially paying the overhead of writing the cache file without ever actually reading it back in.

Even worse, if you try to commit that build info file to Git (please, don't do this), you'll end up in "merge conflict hell" because the file is a binary-ish blob that changes every time anyone saves a file. You aren't caching the result of the work; you're just caching the map of the work, and that map is useless if it's not persisted across your entire team's environment.

Shifting to Content-Addressable Task Caching

To actually move the needle, you have to stop thinking about "compiler settings" and start thinking about "task outputs." In a monorepo—say you have a @company/ui-kit, a @company/api-types, and a @company/web-app—you want to treat each package as a pure function. The inputs are the source files and the dependencies; the output is the dist folder.

The correct strategy is Content-Addressable Caching. Instead of relying on a timestamp or a local TS file, you hash the contents of the package folder. If the hash of @company/ui-kit hasn't changed since the last successful build, you don't run tsc at all. You simply pull the dist folder from a remote cache (like S3 or a dedicated tool like Nx or Turborepo) and move on. This turns a 10-minute build into a 10-second "restore" operation.

// Conceptual logic for a cached build task:
const hash = calculateHash(['src/**/*.ts', 'package.json', 'tsconfig.json']);
if (remoteCache.exists(hash)) {
  remoteCache.download(hash, './dist');
  console.log("Cache hit! Skipping TypeScript compilation.");
} else {
  execSync('tsc -b'); // Use build mode for project references
  remoteCache.upload(hash, './dist');
}


Avoiding the "Ghost Cache" Trap with Proper Input Definitions

Now, here is where most people trip up once they implement the above: they forget about global dependencies. I once spent an entire afternoon debugging a production bug because we were caching our @company/api-types package based only on its own folder. But that package depended on a global typescript version defined in the root package.json. When we upgraded TS from 4.8 to 5.0, the hash for the package didn't change, so the cache kept serving us binaries built with the old version of the compiler.

To avoid this "ghost cache" effect, your cache key must include:

  • The source code of the package.
  • The package.json of the package.
  • The tsconfig.json of the package.
  • The package-lock.json or yarn.lock from the root.
  • The version of the build tool (e.g., the version of tsc).
If any of those change, the hash changes, and you force a re-build. It feels like overkill until the first time you avoid a catastrophic deployment error.


📋 Practical Task

Implement a Hashed Cache Pipeline for the shared-auth Package

You have a monorepo where the packages/shared-auth library is causing slow CI runs. Currently, the CI runs tsc on every single commit, regardless of whether the auth logic changed.

Your Task: Write a Node.js script (cache-build.js) that implements the content-addressable strategy discussed in the lesson. The script should:

  1. Generate a SHA-256 hash based on the contents of packages/shared-auth/src, packages/shared-auth/package.json, and the root package-lock.json.
  2. Check a local directory .build-cache/ to see if a folder named after that hash already exists.
  3. If it exists, copy the contents of that cached folder into packages/shared-auth/dist and exit.
  4. If it doesn't exist, execute tsc -p packages/shared-auth/tsconfig.json, and then copy the resulting dist folder into .build-cache/[hash] for future use.

Note: You can use the crypto module for hashing and fs-extra or fs for file operations.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.