Was ist Turborepo?
Turborepo ist ein hochperformantes Build-System für JavaScript Monorepos. Es führt Tasks über alle Packages in deinem Repository hinweg aus, berücksichtigt dabei die Abhängigkeiten zwischen ihnen und cached die Ergebnisse, sodass unveränderte Arbeit niemals wiederholt wird.
Ein Monorepo ohne Build-System wird schnell mühsam. build und test manuell in jedem Package auszuführen ist langsam, und ein naives Script führt alles erneut aus, selbst wenn sich nur ein einziges Package geändert hat. Turborepo löst beides: Eine deklarative Task-Pipeline übernimmt die Reihenfolge, und Content-basiertes Hashing sorgt dafür, dass unveränderte Tasks effektiv kostenlos sind.
Die Task-Pipeline
Die Pipeline befindet sich in turbo.json. Jeder Task deklariert seine Abhängigkeiten und Outputs.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
dependsOn mit einem Caret (^build) bedeutet „baue zuerst meine Abhängigkeiten“. Ohne das Caret bezieht es sich auf einen Task innerhalb desselben Packages. outputs teilt Turborepo mit, was gecached werden soll, und cache: false schließt einen Task wie dev aus, da dieser endlos läuft.
Tasks ausführen
Ein einziger Befehl führt einen Task über das gesamte Repository hinweg aus – in der richtigen Reihenfolge und, wo möglich, parallel.
turbo run build
turbo run test lint
turbo run dev --filter=web
Turborepo erstellt einen Graph aus dependsOn, führt unabhängige Tasks gleichzeitig aus und stellt abhängige Tasks in eine Warteschlange. Das Ergebnis ist der schnellstmögliche Zeitplan, der dennoch die Abhängigkeiten zwischen den Packages berücksichtigt.
Caching
Caching ist das Feature, das das Gefühl bei der Arbeit mit einem Monorepo grundlegend verändert. Für jeden Task erstellt Turborepo einen Hash der Inputs – Quelldateien, Abhängigkeiten, Umgebungsvariablen und Konfigurationen – und speichert die Outputs sowie die Logs unter diesem Hash.
Beim nächsten Durchlauf wird der Task übersprungen und seine Outputs aus dem Cache wiederhergestellt, sofern sich der Hash nicht geändert hat. Auch die Logs werden erneut ausgegeben, sodass das Ergebnis identisch aussieht, ohne dass die Arbeit erneut erledigt werden muss. Bei einem warmen Cache kann ein vollständiger turbo run build über Dutzende von Packages in wenigen Sekunden abgeschlossen sein.
Remote Caching
Ein lokaler Cache hilft nur einer einzelnen Maschine. Remote Caching teilt den Cache innerhalb des Teams und der CI.
- Ein Entwickler baut ein Paket; das Ergebnis wird hochgeladen.
- Ein anderer Entwickler checkt denselben Commit aus und stellt das Ergebnis sofort wieder her.
- Die CI stellt dieselben Artefakte wieder her und überspringt so Arbeit, die bereits an anderer Stelle erledigt wurde.
Dadurch wird der Cache zu einem gemeinsamen Asset, was oft den größten Geschwindigkeitsvorteil für die CI in einem Monorepo darstellt. Vercel bietet einen gehosteten Remote Cache an, und für Teams, die dies benötigen, gibt es auch Self-Hosted-Optionen.
Filtern
In großen Monorepos muss selten jeder Task ausgeführt werden. Durch Filtern wird eine Teilmenge von Packages angesprochen.
# only packages affected by changes since main
turbo run test --filter="...[origin/main]"
# one package and its dependencies
turbo run build --filter=web...
# only packages that depend on @repo/ui
turbo run build --filter=...@repo/ui
Die [origin/main]-Syntax weist Turborepo an, zu berechnen, welche Packages geändert wurden, und deren Abhängige einzubeziehen. In der CI bedeutet dies, dass ein Pull Request, der nur ein Package betrifft, nur das baut und testet, was tatsächlich davon beeinflusst wird.
Workspaces und Struktur
Turborepo verwaltet Abhängigkeiten nicht selbst – das ist die Aufgabe des Package Managers. Sie definieren Workspaces mit pnpm, npm, yarn oder bun, und Turborepo setzt die Pipeline und den Cache darauf auf.
repo/
├── apps/
│ ├── web/ # a deployable app
│ └── docs/
├── packages/
│ ├── ui/ # a shared component library
│ └── config/ # shared config
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
Apps nutzen gemeinsam verwendete Packages über das Workspace-Protokoll, und Turborepo versteht diesen Dependency Graph bei der Planung der Tasks. Die Kombination aus pnpm Workspaces für das Linking und Turborepo für die Task-Ausführung ist das gängigste moderne Monorepo-Setup.
Best Practices
- Deklariere
outputsfür jede cachebare Aufgabe. - Nutze
dependsOnzusammen mit^, um die Abhängigkeitsreihenfolge festzulegen. - Markiere langlaufende Aufgaben wie
devmitcache: false. - Aktiviere Remote Caching in der CI, um die größten Performance-Gewinne zu erzielen.
- Filtere in der CI nach geänderten Packages, um die Pipelines schnell zu halten.
- Behalte
turbo.jsonim Repository-Root und teile es über alle Packages hinweg. - Füge ein Root-Script hinzu, damit das gesamte Team dieselben Befehle ausführt.
Häufige Fehler
- Das Vergessen von
outputs, wodurch der Cache-Vorteil verloren geht. - Das Ausführen von Tasks in einer beliebigen Reihenfolge, anstatt
dependsOnzu verwenden. - Das Caching von persistenten Tasks, wie zum Beispiel einem Dev-Server.
- Das unnötige Aufnehmen von volatilen Umgebungsvariablen in den Hash.
- Das Ausführen der vollständigen Pipeline in der CI, obwohl ein Filter nicht betroffene Packages überspringen würde.
- Die Behandlung von Turborepo als Ersatz für einen Package-Manager.
Wie geht es weiter?
Turborepo verwandelt ein Monorepo von einer Last in einen Vorteil. Kombiniere es mit pnpm für das Workspace-Linking, verstehe die Grundlagen von npm und Node.js und nutze Vite, um die einzelnen Packages zu builden. Füge anschließend eine root turbo run build zu einem Repository mit mehr als einem Package hinzu und erlebe, wie der zweite Durchlauf fast augenblicklich abgeschlossen ist.