| layout | title | parent | nav_order |
|---|---|---|---|
default |
Chapter 8: Performance Optimization |
Turborepo Tutorial |
8 |
Welcome to Chapter 8: Performance Optimization. In this part of Turborepo Tutorial: High-Performance Monorepo Build System, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
Advanced techniques for maximizing Turborepo performance in large-scale monorepos. As your monorepo grows from a handful of packages to hundreds, maintaining fast build times requires deliberate optimization. This chapter covers profiling, parallelism tuning, graph optimization, package architecture, and the monitoring practices that keep your builds fast over time.
Before optimizing, you need to measure. Turborepo provides several tools for understanding where time is spent.
The --summarize flag produces a detailed JSON report of every task, including timing, cache status, and hash information.
# Generate a run summary
turbo build --summarize
# The summary is saved to .turbo/runs/<run-id>.json
ls .turbo/runs/# Extract task timings from the summary
cat .turbo/runs/*.json | jq '[.tasks[] | {
package: .package,
task: .task,
duration_ms: .execution.duration,
cache: .cache.status,
cache_source: .cache.source
}] | sort_by(-.duration_ms) | .[:10]'| Summary Field | Description | Use For |
|---|---|---|
execution.duration |
Time spent executing the task | Finding slow tasks |
cache.status |
HIT or MISS |
Identifying cache problems |
cache.source |
LOCAL, REMOTE, or null |
Verifying remote cache is working |
hash |
The computed task hash | Debugging cache misses |
inputs |
Files contributing to the hash | Understanding what triggers rebuilds |
environmentVariables |
Env vars in the hash | Diagnosing env-related cache misses |
# Generate an HTML visualization
turbo build --graph=graph.html
# Generate DOT format for Graphviz
turbo build --graph=graph.dot
dot -Tpng graph.dot -o graph.png
# Generate JSON for custom analysis
turbo build --graph=graph.jsonflowchart TD
subgraph "Performance Analysis Workflow"
A[Run with --summarize] --> B[Identify Slow Tasks]
B --> C[Check Cache Hit Rate]
C --> D{Hit Rate > 80%?}
D -->|Yes| E[Optimize Slow Tasks]
D -->|No| F[Fix Cache Configuration]
E --> G[Review Task Graph]
F --> G
G --> H[Optimize Parallelism]
H --> I[Measure Improvement]
I --> A
end
classDef measure fill:#e1f5fe,stroke:#01579b
classDef analyze fill:#fff3e0,stroke:#ef6c00
classDef fix fill:#f3e5f5,stroke:#4a148c
classDef optimize fill:#e8f5e8,stroke:#1b5e20
class A,I measure
class B,C,D analyze
class F fix
class E,G,H optimize
Turborepo automatically parallelizes independent tasks, but you can tune the concurrency level to match your hardware.
# Use all available CPU cores (default)
turbo build --concurrency=100%
# Use a specific number of cores
turbo build --concurrency=4
# Use a percentage of available cores
turbo build --concurrency=50%
# Single-threaded execution (for debugging)
turbo build --concurrency=1
# Set via environment variable
TURBO_CONCURRENCY=8 turbo build| Environment | Recommended Concurrency | Rationale |
|---|---|---|
| Local development (8 cores) | 100% or 10 |
Maximize speed; other work is minimal |
| CI runner (2-4 cores) | 100% or 4 |
Use all available resources |
| CI runner (16+ cores) | 80% or 12 |
Leave headroom for OS and package manager |
| Low-memory CI runner | 2-4 |
Prevent OOM from too many parallel builds |
| Docker build | 100% |
Docker controls CPU allocation |
Each parallel task consumes memory. If you have memory-intensive tasks (TypeScript compilation, webpack bundling), you may need to limit concurrency to prevent out-of-memory errors.
# Increase Node.js memory limit for individual tasks
# Set in the task's package.json script
{
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=4096' next build"
}
}// Or set globally via turbo.json globalPassThroughEnv
{
"globalPassThroughEnv": ["NODE_OPTIONS"]
}The structure of your task graph directly impacts build parallelism. A well-structured graph maximizes the number of tasks that can run concurrently.
flowchart LR
subgraph "Bottlenecked Graph"
A1[pkg-a] --> B1[pkg-b]
B1 --> C1[pkg-c]
C1 --> D1[pkg-d]
D1 --> E1[app]
end
subgraph "Optimized Graph"
A2[pkg-a] --> E2[app]
B2[pkg-b] --> E2
C2[pkg-c] --> E2
D2[pkg-d] --> E2
end
classDef pkg fill:#e1f5fe,stroke:#01579b
classDef app fill:#f3e5f5,stroke:#4a148c
class A1,B1,C1,D1,A2,B2,C2,D2 pkg
class E1,E2 app
The bottlenecked graph forces sequential execution (A -> B -> C -> D -> app), while the optimized graph allows A, B, C, and D to build in parallel.
| Strategy | Description | Impact |
|---|---|---|
| Flatten dependency chains | Reduce intermediate dependencies | More parallel execution |
| Split large packages | Break monolithic packages into smaller ones | Better incremental caching |
Remove unnecessary ^ deps |
Only use ^build when truly needed |
Faster task scheduling |
Use dependsOn carefully |
Avoid over-specifying dependencies | More parallelism |
| Separate config from code | Config packages build fast and unlock dependents | Faster critical path |
// BEFORE: All tasks depend on everything
{
"tasks": {
"build": {
"dependsOn": ["^build", "codegen", "typecheck", "lint"]
}
}
}
// AFTER: Only necessary dependencies
{
"tasks": {
"build": {
"dependsOn": ["^build", "codegen"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {}
}
}In the optimized version, lint and typecheck can run in parallel with build instead of blocking it.
How you structure your packages significantly affects build performance.
Avoid creating a single large shared package that everything depends on:
flowchart TD
subgraph "Anti-Pattern: Monolithic Shared"
S["@repo/shared<br/>(500 files, 2 min build)"]
S --> A1[app-1]
S --> A2[app-2]
S --> A3[app-3]
S --> A4[app-4]
end
subgraph "Better: Granular Packages"
S1["@repo/ui<br/>(50 files)"]
S2["@repo/utils<br/>(30 files)"]
S3["@repo/config<br/>(10 files)"]
S4["@repo/types<br/>(20 files)"]
S1 --> B1[app-1]
S2 --> B1
S1 --> B2[app-2]
S3 --> B2
S2 --> B3[app-3]
S4 --> B3
S1 --> B4[app-4]
S4 --> B4
end
classDef mono fill:#fce4ec,stroke:#e91e63
classDef granular fill:#e8f5e8,stroke:#1b5e20
classDef app fill:#e1f5fe,stroke:#01579b
class S mono
class S1,S2,S3,S4 granular
class A1,A2,A3,A4,B1,B2,B3,B4 app
| Pattern | Build Impact | Cache Impact |
|---|---|---|
| Monolithic shared package | Any change rebuilds everything | Single change busts cache for all apps |
| Granular packages | Only affected packages rebuild | Changes are isolated to specific packages |
| Feature-based packages | Clear ownership, focused builds | High cache hit rate |
| Metric | Guideline | Rationale |
|---|---|---|
| Files per package | 20-100 | Large enough to be useful, small enough for good caching |
| Build time per package | < 30 seconds | Long builds block the dependency graph |
| Dependencies per package | < 5 internal | Fewer dependencies = more parallelism |
| Consumers per package | Any | Shared packages can have many consumers |
TypeScript project references enable incremental compilation, which pairs well with Turborepo's caching:
// tsconfig.json (root)
{
"references": [
{ "path": "packages/config" },
{ "path": "packages/utils" },
{ "path": "packages/ui" },
{ "path": "apps/web" }
],
"files": []
}// packages/ui/tsconfig.json
{
"extends": "@repo/config/tsconfig/react-library",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declarationMap": true
},
"include": ["src/**/*"],
"references": [
{ "path": "../config" },
{ "path": "../utils" }
]
}Fine-tuning inputs and outputs has the single largest impact on cache hit rates.
{
"tasks": {
"build": {
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"!src/**/*.test.ts",
"!src/**/*.test.tsx",
"!src/**/*.spec.ts",
"!src/**/*.stories.tsx",
"!src/**/__tests__/**",
"!src/**/__mocks__/**",
"!src/**/__fixtures__/**",
"package.json",
"tsconfig.json"
],
"outputs": ["dist/**"]
},
"test": {
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"test/**",
"__tests__/**",
"jest.config.*",
"vitest.config.*",
"!**/*.stories.tsx"
],
"outputs": ["coverage/**"]
}
}
}Only cache what is necessary. Excluding cache directories and temporary files reduces storage and transfer time:
{
"tasks": {
"build": {
"outputs": [
"dist/**",
"!dist/**/*.map",
"!dist/**/*.tsbuildinfo"
]
},
"build:next": {
"outputs": [
".next/**",
"!.next/cache/**",
"!.next/trace"
]
}
}
}# Check the size of cached artifacts
du -sh node_modules/.cache/turbo/*
# Count files in outputs
find dist -type f | wc -l
# Measure restore time
time turbo build # Second run should be a cache hitDevelopment speed depends on fast iteration cycles. Turborepo's watch mode and persistent tasks are key to this.
{
"tasks": {
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
},
"dev:css": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}# Watch mode rebuilds on file changes
turbo watch build
# Run dev server + watch dependent packages
turbo dev --filter=@repo/web...
# Watch with the terminal UI for better visibility
turbo watch build --ui=tuiMark persistent tasks as interruptible so they can be restarted when dependencies change in watch mode:
{
"tasks": {
"dev": {
"cache": false,
"persistent": true,
"interruptible": true
}
}
}| Monorepo Size | Packages | Typical Cold Build | With Turborepo Cache | Optimization Focus |
|---|---|---|---|---|
| Small | 5-15 | 2-5 min | < 30 sec | Basic caching |
| Medium | 15-50 | 5-15 min | 1-3 min | Input/output tuning |
| Large | 50-200 | 15-45 min | 2-5 min | Graph optimization, parallelism |
| Enterprise | 200+ | 45+ min | 3-10 min | Package architecture, distributed caching |
| Stage | Actions |
|---|---|
| Starting (5-15 packages) | Enable remote caching, configure basic inputs/outputs |
| Growing (15-50 packages) | Optimize inputs for cache hit rate, set up CI with --filter |
| Large (50-200 packages) | Split monolithic packages, optimize task graph, tune concurrency |
| Enterprise (200+) | Implement module boundaries, custom cache server, dedicated CI runners |
# Find the largest packages by dependency count
turbo query "query {
packages {
name
dependencies { name }
dependents { name }
}
}"
# Find packages with no dependents (potential candidates for removal)
turbo query "query {
packages(filter: { dependentCount: 0 }) {
name
}
}"The performance of individual build tools within each package directly affects overall monorepo build time.
| Tool | Use Case | Speed | Configuration |
|---|---|---|---|
| tsup | Library bundling | Fast | Minimal |
| esbuild | Bundling (JS/TS) | Very fast | Moderate |
| SWC | TypeScript compilation | Very fast | Minimal |
| tsc | Type checking / declaration files | Moderate | Standard |
| Vite | App bundling + dev server | Fast | Moderate |
| webpack | Complex app bundling | Slower | Complex |
| Next.js (with SWC) | Full-stack React apps | Fast | Framework-managed |
// tsconfig.json optimizations for speed
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"skipLibCheck": true,
"isolatedModules": true
}
}| tsconfig Option | Performance Impact | Trade-off |
|---|---|---|
incremental: true |
Faster subsequent builds | Stores .tsbuildinfo file |
skipLibCheck: true |
Skips type checking .d.ts files |
May miss type errors in dependencies |
isolatedModules: true |
Enables parallel file processing | Minor syntax restrictions |
composite: true |
Enables project references | Required for project references |
// next.config.js -- Next.js uses SWC by default
/** @type {import('next').NextConfig} */
module.exports = {
swcMinify: true,
// SWC is used by default for compilation
}// .swcrc for standalone SWC usage
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true
},
"transform": {
"react": {
"runtime": "automatic"
}
},
"target": "es2020"
},
"module": {
"type": "es6"
}
}Track these metrics over time to detect performance regressions:
# Script to collect build metrics
#!/bin/bash
START_TIME=$(date +%s%3N)
turbo build --summarize 2>&1
END_TIME=$(date +%s%3N)
DURATION=$((END_TIME - START_TIME))
SUMMARY=$(cat .turbo/runs/*.json | jq '{
total_tasks: .tasks | length,
cached_tasks: [.tasks[] | select(.cache.status == "HIT")] | length,
cache_hit_rate: (([.tasks[] | select(.cache.status == "HIT")] | length) / (.tasks | length) * 100),
total_duration_ms: '$DURATION'
}')
echo "$SUMMARY"
# Send to your metrics system (Datadog, Prometheus, etc.)| KPI | Target | Warning | Critical |
|---|---|---|---|
| CI pipeline time (cached) | < 5 min | 5-10 min | > 10 min |
| Cache hit rate (CI) | > 80% | 60-80% | < 60% |
| Cold build time | < 15 min | 15-30 min | > 30 min |
| Install time | < 2 min | 2-5 min | > 5 min |
| Largest package build | < 30 sec | 30-60 sec | > 60 sec |
| Task graph depth | < 5 levels | 5-8 levels | > 8 levels |
# .github/workflows/perf-check.yml
name: Build Performance Check
on:
pull_request:
branches: [main]
jobs:
perf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Cold build timing
run: |
START=$(date +%s)
pnpm turbo build --force --summarize
END=$(date +%s)
DURATION=$((END - START))
echo "Cold build time: ${DURATION}s"
if [ $DURATION -gt 900 ]; then
echo "::warning::Cold build time exceeds 15 minutes (${DURATION}s)"
fi
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Upload build summary
uses: actions/upload-artifact@v4
with:
name: build-summary
path: .turbo/runs/flowchart TD
A[PR Opened] --> B[Cold Build]
B --> C[Collect Metrics]
C --> D{Build Time OK?}
D -->|Yes| E[PR Passes]
D -->|No| F[Warning Comment on PR]
C --> G[Store Metrics]
G --> H[Dashboard / Alerting]
classDef trigger fill:#e1f5fe,stroke:#01579b
classDef measure fill:#fff3e0,stroke:#ef6c00
classDef pass fill:#e8f5e8,stroke:#1b5e20
classDef warn fill:#fce4ec,stroke:#e91e63
class A trigger
class B,C,G measure
class D measure
class E pass
class F,H warn
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["NODE_ENV", "BUILD_TARGET"]
}
}
}# Development build (fast, no minification)
NODE_ENV=development turbo build
# Production build (optimized, minified)
NODE_ENV=production turbo build
# The different NODE_ENV values produce different hashes,
# so each environment has its own cache entries# Skip E2E tests unless explicitly requested
turbo build test lint
# Run E2E only when the label is present or on main
turbo build test lint test:e2e --filter=...[origin/main]// packages/ui/turbo.json
{
"extends": ["//"],
"tasks": {
"build": {
"inputs": [
"src/**",
"$TURBO_ROOT$/tsconfig.base.json",
"$TURBO_ROOT$/tailwind.config.ts"
]
}
}
}| Category | Optimization | Impact | Effort |
|---|---|---|---|
| Caching | Enable remote caching | Very High | Low |
| Caching | Configure precise inputs |
High | Medium |
| Caching | Exclude test files from build inputs |
Medium | Low |
| Caching | Declare all env variables |
High | Low |
| Parallelism | Remove unnecessary dependsOn |
High | Medium |
| Parallelism | Tune --concurrency for CI |
Medium | Low |
| Architecture | Split monolithic packages | Very High | High |
| Architecture | Use TypeScript project references | Medium | Medium |
| Build tools | Switch to SWC/esbuild for compilation | High | Medium |
| Build tools | Enable TypeScript incremental |
Medium | Low |
| CI/CD | Cache dependency installation | High | Low |
| CI/CD | Use --filter for affected packages |
Very High | Low |
| CI/CD | Docker multi-stage with turbo prune |
High | Medium |
| Monitoring | Track cache hit rates over time | Medium | Low |
Performance optimization in Turborepo is an ongoing discipline that spans caching configuration, task graph design, package architecture, and build tool selection. The highest-impact optimizations are enabling remote caching, configuring precise inputs, and using change-detection filters in CI. As your monorepo grows, invest in splitting large packages, monitoring build metrics, and automating performance regression detection to keep builds fast at any scale.
- Measure before optimizing: Use
--summarizeand--graphto understand where time is spent before making changes. - Cache hit rate is the primary metric: Target above 80% in CI; debug misses systematically with hash comparison.
- Parallelism is bounded by your graph: Flatten dependency chains and remove unnecessary
dependsOnto unlock more parallel execution. - Package size matters: Granular packages (20-100 files) produce better caching and parallelism than monolithic ones.
- Build tool choice affects speed: Use SWC, esbuild, or tsup instead of slower alternatives like Babel or raw tsc for production builds.
- Monitor continuously: Track build times, cache hit rates, and package counts over time to catch regressions early.
- Invest proportionally: Small repos need basic caching; enterprise repos need architecture work and dedicated monitoring.
Congratulations on completing the Turborepo tutorial. You have learned to:
- Set up and configure a Turborepo monorepo from scratch
- Design workspace structures with proper package organization
- Build task pipelines with dependency management and parallel execution
- Implement caching strategies for maximum cache hit rates
- Deploy remote caching for team-wide build acceleration
- Manage dependencies across internal and external packages
- Integrate with CI/CD platforms for automated, efficient pipelines
- Optimize performance at every level of the build system
| Direction | Resources |
|---|---|
| Stay current | Turborepo Blog for new releases |
| Deep dive | Turborepo API Reference |
| Community | Turborepo GitHub Discussions |
| Enterprise | Vercel Enterprise for managed solutions |
| Contribute | Turborepo Contributing Guide |
Built with insights from the Turborepo project.
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for build, turbo, json so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 8: Performance Optimization as an operating subsystem inside Turborepo Tutorial: High-Performance Monorepo Build System, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around tasks, cache, classDef as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 8: Performance Optimization usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
build. - Input normalization: shape incoming data so
turboreceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
json. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- View Repo
Why it matters: authoritative reference on
View Repo(github.com).
Suggested trace strategy:
- search upstream code for
buildandturboto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production