Mit .tsbuildinfo, composite und Project References
Große TypeScript Projekte leiden oft unter langsamen Builds, weil der Compiler bei jedem Lauf sämtliche Dateien erneut vollständig prüft und Typen neu berechnet. Dieser Artikel zeigt praxisnah, wie der Cache tsbuildinfo, die Flags incremental und composite sowie Project References mit tsc build Kompilierzeiten in Monorepos und großen Codebasen spürbar reduzieren, inklusive konkreter Diagnose und Messmethoden für hartnäckige Engpässe im Alltag.
Inhaltsverzeichnis
- 1. Warum TypeScript-Builds in großen Projekten langsam werden
- 2. Das incremental-Flag: Wie der Compiler Neuprüfungen vermeidet
- 3. Der Build-Info-Cache: Aufbau und Inhalt von .tsbuildinfo
- 4. composite und Project References: Codebasen sauber aufteilen
- 5. Das references-Array: Abhängigkeiten zwischen Teilprojekten
- 6. tsc --build: Orchestrierung über mehrere Projekte hinweg
- 7. Langsame Builds diagnostizieren mit --extendedDiagnostics
- 8. Tiefenanalyse mit --generateTrace und dem Trace-Analyzer
- 9. Build-Strategien im Vergleich: Full Rebuild vs. Incremental vs. Project References
- 10. Zusammenfassung
- 11. FAQ
1. Warum TypeScript-Builds in großen Projekten langsam werden
Der TypeScript Compiler tsc prüft standardmäßig bei jedem Lauf sämtliche Dateien im Projekt neu, unabhängig davon, ob sich seit dem letzten Build überhaupt etwas geändert hat. Für ein kleines Skript ist das kein Problem, aber in einer gewachsenen Codebasis mit mehreren tausend Dateien, komplexen generischen Typen und tiefen Importketten summiert sich die Typprüfung schnell auf mehrere Minuten pro Durchlauf. Besonders schmerzhaft wird das in CI-Pipelines, wo jeder Build kalt startet und der Compiler ohne jeden Kontext aus vorherigen Läufen von vorne beginnt.
Das Problem verschärft sich mit der Struktur vieler moderner Projekte: Monorepos mit mehreren Paketen, geteilten Utility-Bibliotheken und Frontend- sowie Backend-Code in einem Repository führen dazu, dass eine einzige Änderung in einer zentralen Datei theoretisch tausende abhängige Dateien betreffen könnte. tsc kennt aus sich heraus keine Grenzen zwischen logischen Modulen, es sieht nur die im tsconfig.json referenzierten Dateien als eine große, zusammenhängende Kompiliereinheit. Genau hier setzen incremental Compilation und Project References an: Sie geben dem Compiler die Information, welche Teile der Codebasis unabhängig voneinander geprüft und zwischengespeichert werden können.
2. Das incremental-Flag: Wie der Compiler Neuprüfungen vermeidet
Mit "incremental": true in der tsconfig.json weist man tsc an, nach jedem erfolgreichen Build Informationen über den Zustand des Projekts in einer Cache-Datei zu speichern. Beim nächsten Aufruf liest der Compiler diese Datei zuerst ein und vergleicht Zeitstempel und Dateihashes mit dem aktuellen Stand des Dateisystems. Nur Dateien, die sich tatsächlich geändert haben, sowie Dateien, die von diesen Änderungen transitiv betroffen sind, werden erneut typgeprüft. Alle anderen Ergebnisse werden direkt aus dem Cache übernommen, ohne dass der Compiler den Syntaxbaum neu aufbauen oder Typen neu auflösen muss.
Der Geschwindigkeitsgewinn hängt stark vom Änderungsradius ab: Wer eine isolierte Utility-Funktion ohne viele Abhängigkeiten ändert, profitiert enorm, während eine Änderung an einem zentralen Typ, der in hundert Dateien importiert wird, weiterhin eine breite Neuprüfung auslöst. In der Praxis lohnt sich incremental bereits ab mittelgroßen Projekten, weil selbst bei größeren Änderungsradien der reine Parsing- und Dateisystem-Overhead entfällt. Wichtig ist, die generierte Cache-Datei nicht versehentlich einzuchecken oder zwischen inkompatiblen TypeScript-Versionen wiederzuverwenden, da veraltete Caches sonst verworfen und stillschweigend neu aufgebaut werden.
3. Der Build-Info-Cache: Aufbau und Inhalt von .tsbuildinfo
Die Datei .tsbuildinfo, die durch incremental entsteht, ist kein binäres Blackbox-Format, sondern eine JSON-Struktur mit klar abgrenzbaren Abschnitten: einer Liste aller Quelldateien, den daraus resultierenden Programm-Optionen, einer Datei-Versions-Tabelle mit Hashes sowie einer Referenztabelle, die festhält, welche Datei von welcher anderen Datei abhängt. Genau diese Abhängigkeitstabelle ist der Kern der Geschwindigkeit: Ändert sich eine Datei, muss tsc nicht raten, welche anderen Dateien betroffen sein könnten, sondern kann die Antwort direkt aus dem Graphen ablesen.
Der Speicherort lässt sich über tsBuildInfoFile explizit festlegen, was besonders bei mehreren Teilprojekten mit eigenen tsconfig.json-Dateien wichtig ist, um Kollisionen zu vermeiden. In CI-Umgebungen empfiehlt es sich, dieses Verzeichnis gezielt zwischen Builds zwischenzuspeichern, etwa über den Cache-Mechanismus von GitHub Actions oder GitLab CI, denn ein warmer Build-Info-Cache kann die Build-Zeit auf einen Bruchteil reduzieren. Fehlt die Datei beim ersten Lauf, führt tsc automatisch einen vollständigen Build durch und legt den Cache für alle folgenden Läufe neu an, ohne dass dafür ein manueller Eingriff nötig ist.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"declaration": true,
"declarationMap": true,
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsc/app.tsbuildinfo",
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}
4. composite und Project References: Codebasen sauber aufteilen
"composite": true aktiviert einen strengeren Modus, der für Project References vorausgesetzt wird. Composite-Projekte müssen alle Eingabedateien explizit über include oder files deklarieren, jede importierte Datei muss Teil desselben Projekts sein, und Deklarationsdateien (.d.ts) werden zwingend erzeugt, damit andere Projekte die öffentliche API konsumieren können, ohne die Quelldateien selbst neu zu parsen. Composite impliziert außerdem automatisch incremental, sodass beide Mechanismen in der Praxis meist gemeinsam auftreten.
Der eigentliche Wert von composite zeigt sich erst in Kombination mit mehreren Teilprojekten: Statt eines einzigen riesigen tsconfig.json mit tausenden Dateien entstehen mehrere kleinere, unabhängig kompilierbare Einheiten, etwa ein core-Paket, ein utils-Paket und ein app-Paket, die jeweils ihre eigene .tsbuildinfo besitzen. Ändert sich nur der app-Code, muss der Compiler weder core noch utils erneut prüfen, sondern lädt deren bereits erzeugte Deklarationsdateien direkt aus dem Output-Verzeichnis. Das reduziert nicht nur die Build-Zeit, sondern auch den Speicherverbrauch des Compiler-Prozesses erheblich, da nicht mehr der gesamte Programmgraph gleichzeitig im Arbeitsspeicher gehalten werden muss.
5. Das references-Array: Abhängigkeiten zwischen Teilprojekten
Das references-Array in der tsconfig.json verweist auf die Verzeichnisse anderer composite-Projekte, von denen das aktuelle Projekt abhängt. tsc nutzt diese Information, um einen Abhängigkeitsgraphen auf Projektebene aufzubauen, unabhängig vom Datei-Abhängigkeitsgraphen innerhalb eines einzelnen Projekts. Wichtig dabei: Referenzierte Projekte müssen composite gesetzt haben, und im referenzierenden Code dürfen nur die exportierten, in den Deklarationsdateien sichtbaren Typen verwendet werden, interne, nicht exportierte Implementierungsdetails sind nicht sichtbar.
In der Praxis ergibt sich daraus eine natürliche Architekturgrenze: Wer versucht, ein internes Hilfsmodul eines anderen Pakets zu importieren, das nicht Teil der öffentlichen API ist, bekommt einen klaren Compilerfehler statt einer stillschweigenden, aber fragilen Abhängigkeit. Zusätzlich lässt sich disableReferencedProjectLoad nutzen, um in IDEs die automatische Ladung aller referenzierten Projekte zu unterbinden und so die Editor-Performance in sehr großen Monorepos zu verbessern, während tsc --build weiterhin alle Referenzen korrekt respektiert.
// tsconfig.json (repo root, solution style, no compilerOptions of its own)
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/utils" },
{ "path": "./packages/app" }
]
}
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
// packages/app/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../core" },
{ "path": "../utils" }
],
"include": ["src/**/*.ts"]
}
6. tsc --build: Orchestrierung über mehrere Projekte hinweg
Der reguläre tsc-Aufruf ignoriert das references-Array größtenteils und kompiliert nur das aktuelle Projekt. Erst tsc --build, kurz tsc -b, versteht die Abhängigkeitsstruktur vollständig: Es ermittelt topologisch die richtige Build-Reihenfolge, baut zuerst die Projekte ohne eigene Abhängigkeiten und arbeitet sich dann durch den Graphen nach oben, wobei bereits aktuelle Projekte anhand ihrer .tsbuildinfo automatisch übersprungen werden. Das Ergebnis ist ein Build, der sich über das gesamte Monorepo hinweg wie ein einziger inkrementeller Vorgang verhält, obwohl technisch viele einzelne tsc-Prozesse beteiligt sind.
Nützliche Flags im Alltag: --verbose zeigt an, welche Projekte gebaut und welche übersprungen werden, --force erzwingt einen vollständigen Rebuild aller referenzierten Projekte unabhängig vom Cache-Status, und --clean entfernt alle Build-Outputs und Cache-Dateien der referenzierten Projekte wieder. Für Watch-Modus-Entwicklung existiert tsc --build --watch, das Änderungen über Projektgrenzen hinweg verfolgt und nur die tatsächlich betroffenen Teilprojekte neu baut, was insbesondere in großen Frontend-Monorepos den Feedback-Zyklus drastisch verkürzt.
# Build the whole graph, respecting topological project order
tsc --build
# Show which projects were rebuilt and which were skipped as up to date
tsc --build --verbose
# Force a full rebuild of every referenced project, ignore .tsbuildinfo
tsc --build --force
# Remove all build outputs and .tsbuildinfo files for referenced projects
tsc --build --clean
# Watch mode across project boundaries, rebuild only affected projects
tsc --build --watch
7. Langsame Builds diagnostizieren mit --extendedDiagnostics
--extendedDiagnostics liefert eine detaillierte Aufschlüsselung, wo der Compiler tatsächlich Zeit verbringt: Parsing-Zeit, Binding-Zeit, Typprüfungszeit, Emit-Zeit sowie separate Zähler für die Anzahl geprüfter Typen, aufgelöster Module und verarbeiteter Symbole. Diese Ausgabe ist der erste Anlaufpunkt, wenn ein Build unerwartet langsam ist, da sie sofort zeigt, ob das Problem in der reinen Typprüfung liegt, in der Modulauflösung oder im Emit der Ausgabedateien.
Ein häufiges Muster: Eine hohe Zahl an "Types" bei gleichzeitig hoher "Check time" deutet auf komplexe, tief verschachtelte generische Typen hin, die sich oft durch gezieltes Vereinfachen von Utility-Types entschärfen lassen. Eine hohe "Module resolution"-Zeit weist dagegen häufig auf ein ungünstig konfiguriertes paths-Mapping oder auf zu viele durchsuchte node_modules-Verzeichnisse hin. Kombiniert mit --diagnostics, das eine kompaktere Kurzfassung liefert, lässt sich so schnell entscheiden, ob eine Optimierung an der tsconfig.json, an der Ordnerstruktur oder tatsächlich an den Typen selbst ansetzen sollte.
tsc --noEmit --extendedDiagnostics
# Files: 842
# Lines of Library: 41230
# Lines of Definitions: 18904
# Lines of Source: 52117
# Types: 184220
# Instantiations: 612044
# Symbols: 298511
# Parse time: 1.42s
# Bind time: 0.61s
# Check time: 9.83s <- dominant cost, inspect types
# Emit time: 0.94s
# Total time: 12.80s
# Check time dominates: look for deeply nested generics or heavy
# conditional types before touching module resolution settings.
8. Tiefenanalyse mit --generateTrace und dem Trace-Analyzer
Wenn --extendedDiagnostics nur grobe Kategorien liefert, aber die konkrete Ursache unklar bleibt, hilft --generateTrace. Der Flag schreibt eine Chrome-Tracing-kompatible Datei, die sich direkt in chrome://tracing oder im offiziellen @typescript/analyze-trace-Tool öffnen lässt und jeden einzelnen Typprüfungsschritt zeitlich aufgelöst darstellt, inklusive der exakten Datei und Position im Code, an der ein bestimmter Typ aufgelöst wurde.
Besonders wertvoll ist das Trace-File bei sogenannten "Hot Types", also einzelnen Typdefinitionen, deren Auflösung an vielen verschiedenen Stellen im Code überproportional viel Zeit kostet, etwa durch exzessive Nutzung rekursiver Conditional Types oder tief verschachtelter Mapped Types. Das analyze-trace-Tool listet solche Typen automatisch nach Gesamtzeit sortiert auf und liefert damit konkrete Ansatzpunkte, statt dass man Typen im Code raten und einzeln testen muss. In Kombination mit incremental Compilation lohnt sich diese Analyse besonders für Typen, die in jedem Build erneut aufgelöst werden müssen, weil sie an zentraler Stelle in vielen Dateien importiert werden.
# Write a Chrome-tracing compatible trace plus a type catalog
tsc --build --generateTrace trace-output
# Install and run the official trace analyzer on the output folder
npm install --no-save @typescript/analyze-trace
npx analyze-trace trace-output
# Example finding: a single mapped type resolved 4200 times
# across the codebase, costing 2.1s of the total check time
9. Build-Strategien im Vergleich: Full Rebuild vs. Incremental vs. Project References
Die drei Ansätze unterscheiden sich deutlich in Aufwand, Wartbarkeit und tatsächlichem Geschwindigkeitsgewinn. Die folgende Übersicht fasst zusammen, wann sich welche Strategie lohnt und was jeweils die typischen Stolpersteine sind.
| Strategie | Rebuild bei 1 Datei-Änderung | Typisches Problem | Empfehlung |
|---|---|---|---|
| tsc ohne incremental | Immer vollständig | Jede noch so kleine Änderung kostet volle Zeit | Nur für isolierte Skripte oder Release-Builds |
| tsc mit incremental | Nur betroffene Dateien | .tsbuildinfo versehentlich gelöscht oder eingecheckt | .gitignore pflegen, Cache in CI persistieren |
| composite ohne tsc --build | Manuelle Reihenfolge nötig | Falsche Build-Reihenfolge erzeugt veraltete .d.ts | composite immer mit tsc --build kombinieren |
| tsc --build (References) | Nur betroffene Teilprojekte | Fehlende Referenz im references-Array | Projektgrenzen an echten Modulgrenzen ausrichten |
| tsc --build --watch | Sekundenbruchteile | Hoher Speicherverbrauch bei vielen Watchern | Für lokale Entwicklung in großen Monorepos |
In der Praxis ist die Kombination aus composite, references und tsc --build der Standard für Monorepos ab einer gewissen Größe, während ein einzelnes incremental-Projekt für kleinere, in sich geschlossene Anwendungen völlig ausreicht. Der entscheidende Fehler ist selten die Wahl der falschen Strategie, sondern eine inkonsequente Umsetzung, etwa fehlende Cache-Persistenz in CI oder Projektgrenzen, die nicht den tatsächlichen Modulgrenzen im Code entsprechen.
Mironsoft
TypeScript-Tooling, Build-Performance und Monorepo-Architektur für Magento- und Headless-Projekte
TypeScript-Builds professionell beschleunigen?
Wir analysieren eure tsc-Builds, richten incremental Compilation und Project References sauber ein und schneiden Monorepos entlang echter Modulgrenzen zu, von der tsconfig-Strategie bis zur CI-Cache-Konfiguration.
Build-Performance-Audit
extendedDiagnostics- und Trace-Analyse, Priorisierung nach Zeitgewinn
Monorepo-Zuschnitt
Project References entlang sauberer Paketgrenzen strukturieren
CI-Cache-Setup
.tsbuildinfo-Persistenz und tsc --build in der Pipeline verankern
10. Zusammenfassung
Die Incremental Compilation in TypeScript löst ein Kernproblem großer Codebasen: Ohne Cache prüft tsc bei jedem Lauf alles neu, unabhängig vom tatsächlichen Änderungsumfang. Mit "incremental": true und der daraus entstehenden .tsbuildinfo-Datei muss der Compiler nur noch geänderte und transitiv betroffene Dateien erneut prüfen. composite und das references-Array gehen einen Schritt weiter und teilen die Codebasis in unabhängig kompilierbare Teilprojekte, die tsc --build in korrekter Reihenfolge orchestriert und dabei bereits aktuelle Projekte automatisch überspringt.
Für die Diagnose stehen zwei komplementäre Werkzeuge bereit: --extendedDiagnostics liefert schnell einen groben Überblick über Parsing-, Bind-, Check- und Emit-Zeiten, während --generateTrace gemeinsam mit dem analyze-trace-Tool bis auf einzelne Typen und Dateipositionen herunterbricht. Wer beide Werkzeuge kombiniert einsetzt, findet die tatsächlichen Engpässe statt an tsconfig-Optionen zu raten, und kann Monorepos so zuschneiden, dass Build-Zeiten mit der Projektgröße nicht mehr linear, sondern nur noch mit dem tatsächlichen Änderungsradius wachsen.
Incremental Compilation in TypeScript - Das Wichtigste auf einen Blick
incremental + tsBuildInfoFile
Cache-Datei speichert Datei-Hashes und Abhängigkeitsgraph, nur geänderte Dateien werden neu geprüft.
composite + references
Codebasis in unabhängig kompilierbare Teilprojekte mit eigenen .d.ts-Ausgaben aufteilen.
tsc --build
Orchestriert die richtige Build-Reihenfolge und überspringt bereits aktuelle Projekte automatisch.
Diagnose & Messung
--extendedDiagnostics für den Überblick, --generateTrace für die Tiefenanalyse einzelner Typen.