javascript #monorepo #turborepo #nx
Most teams move to a monorepo and see zero speed gains because they copy the README setup and stop. The real wins come from a few configuration patterns that most repos never implement.
Here are 6 concrete Turborepo and Nx patterns that actually reduce CI time and developer wait time in production projects.
1. Upstream Build Dependencies Instead of Global Builds
If your build task does not declare upstream dependencies, your cache graph is wrong.
Before – naive Turborepo setup
{
"tasks": {
"build": {
"outputs": ["dist/**"]
}
}
}
This builds every package independently. If ui depends on shared-types, build order is undefined.
After – dependency aware build graph
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
}
}
}
The ^build ensures dependencies build first. Turborepo now executes in parallel where possible but preserves correctness.
In a 12 package repo, this alone removed 4 to 6 unnecessary rebuilds per CI run.
2. Filter Changed Packages in CI Instead of Building Everything
The biggest monorepo mistake is running full builds on every PR.
Before – full CI run
- run: npx turbo build
- run: npx turbo test
Every package runs. Every time.
After – change based filtering (Turborepo)
- run: npx turbo build --filter='...[HEAD^1]'
- run: npx turbo test --filter='...[HEAD^1]'
Now only changed packages and their dependents run.
Nx equivalent
- run: npx nx affected --target=build
- run: npx nx affected --target=test
On a 20 package workspace, this typically drops CI from 14 minutes to 3 to 5 minutes.
This kind of pipeline optimization compounds with the architectural decisions described in the JavaScript application architecture patterns for scalable systems when your repo grows beyond a single deployable app.
3. Remote Caching for Team-Wide Build Deduplication
Local caching helps one developer. Remote caching helps the entire team.
Before – local only
npx turbo build
Every developer rebuilds the same unchanged packages.
After – remote cache enabled (Turborepo)
npx turbo login
npx turbo link
Now build artifacts are shared.
Nx equivalent
npx nx connect-to-nx-cloud
On a 6 developer team, remote caching reduced cumulative daily build time by roughly 40%. That is not theoretical. That is hours per week returned.
4. TypeScript Project References for Incremental Type Checking
Monorepos with 15 plus TypeScript packages can spend 30 seconds on type checking alone.
Before – flat TypeScript config
{
"compilerOptions": {
"outDir": "dist"
}
}
Every package compiles from scratch.
After – composite + references
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist"
},
"references": [
{ "path": "../shared-types" }
]
}
Then run:
tsc --build
TypeScript now builds in dependency order and caches results. Incremental builds drop from 25 seconds to under 5 in medium sized repos.
Neither Turborepo nor Nx sets this up for you. You must configure it intentionally.
5. Enforce Package Boundaries to Prevent Graph Explosion
Performance degrades when everything imports everything.
Before – no constraints
// libs/ui importing from apps/web
import { something } from '../../apps/web/internal'
This creates circular and cross layer coupling.
After – Nx enforce-module-boundaries
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:lib"] }
]
}
]
}
}
Now apps cannot be imported into libraries.
Turborepo does not enforce this. You enforce it through ESLint rules or strict folder conventions.
In large workspaces, clean boundaries reduce affected graph size by 20 to 30%, which directly reduces CI scope.
6. Separate Dev Tasks From Cached Tasks
Caching dev servers is useless and slows things down.
Before – dev cached unintentionally
{
"tasks": {
"dev": {}
}
}
Turborepo may try to treat it like a regular task.
After – persistent and uncached
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}
Now local development runs cleanly without polluting cache state.
In Nx, you achieve similar separation by ensuring serve targets are not part of affected production pipelines.
This prevents confusing cache invalidations that cause developers to distrust the system.
Turborepo vs Nx for Performance-Critical Workspaces
If you have fewer than 15 packages and strong existing tooling, Turborepo is usually enough. It accelerates what you already have.
If you have 30 plus packages and cross domain imports, Nx often wins because its project graph analyzes actual file imports instead of just package.json dependencies. That finer granularity means fewer false positives in affected builds.
But neither tool magically optimizes your repo. The speed gains come from:
- Correct dependency declarations
- Aggressive change filtering
- Remote caching
- Incremental TypeScript builds
- Strict package boundaries
If your CI is still running full builds on every push, you do not have a monorepo problem. You have a configuration problem.
Pick one of these patterns and implement it this week. Measure CI before and after. If you cannot show a delta in minutes saved, you are not using your monorepo tooling correctly.
Top comments (0)