This repository demonstrates advanced build and test caching optimizations using Turborepo and Next.js with Turbopack. The goal is to achieve deterministic builds and intelligent test caching that respects tree-shaking and only invalidates tests when the actual build output changes.
This project tests whether we can optimize Turborepo caching to:
- Avoid unnecessary rebuilds when changes in shared packages don't affect consuming apps
- Reuse test cache when build outputs remain identical (even if builds ran)
- Leverage tree-shaking to ensure unused code doesn't affect build artifacts
- Apps:
apps/webandapps/docs(Next.js applications) - Shared Package:
packages/ui(@repo/ui) - shared UI components and utilities - E2E Tests:
packages/web-e2eandpackages/docs-e2e- test packages that depend on build outputs
apps/webimportssubfrom@repo/ui/utilsapps/docsimportsaddfrom@repo/ui/utils- Both apps import from the barrel file
@repo/ui/utils/index.tswhich exports bothaddandsub
The optimization leverages Next.js optimizePackageImports and package-level sideEffects: false to enable aggressive tree-shaking:
- Barrel File Exports:
packages/ui/src/utils/index.tsexports bothaddandsub - Selective Imports: Each app only imports what it uses (
web→sub,docs→add) - Tree-Shaking: Next.js/Turbopack removes unused exports from the final bundle
- Result: Changing
add.tsonly affectsdocsbuild output, notwebbuild output
The scripts/hash-build.ts script calculates content hashes of build outputs (.next directories) and writes them to build.hash files. The e2e:test task uses these hashes as inputs instead of depending on ^build:
- If build output changes → hash changes →
build.hashupdates → e2e test cache invalidates - If build output is identical → hash unchanged →
build.hashunchanged → e2e test cache hits
Run the e2e tests twice in a row:
bun e2e:test
bun e2e:testExpected Result:
- First run: All builds and tests run (4 tasks total)
- Second run: All tasks use cache (4 cached results)
Modify the add method in packages/ui/src/utils/add.ts:
# Edit packages/ui/src/utils/add.ts (change the implementation)
bun e2e:testExpected Result:
- Builds: Both
webanddocsrebuild (cache miss - correct, as Turborepo tracks dependency changes) - Build Outputs:
docsbuild output changes (usesadd)webbuild output remains identical (doesn't useadd, tree-shaking removes it)
- E2E Tests:
docs-e2eruns fresh (itsbuild.hashchanged)web-e2euses cache (itsbuild.hashunchanged)
This demonstrates that even though both apps rebuild, only the app that actually uses the changed code has its test cache invalidated.
Barrel files (index.ts) re-export multiple modules from a single entry point. With proper tree-shaking:
- Without tree-shaking: Importing
{ sub }from@repo/ui/utilswould bundle bothaddandsub - With tree-shaking: Only
subis included in the bundle,addis eliminated
The key configuration enabling this:
- Package config (
packages/ui/package.json):"sideEffects": false - Next.js config:
optimizePackageImports: ["@repo/ui"] - Build tool: Turbopack (
--turboflag) for production builds
bun e2e:test- Runs builds, syncs build hashes, then runs e2e testsbun build- Builds all apps and packagesbun dev- Starts development servers
See spec/OPTIMIZATION_PLAN.md for the detailed implementation plan and rationale.
See spec/HASHING_STRATEGY.md for the hashing strategy used to calculate the build hash..