Beschleunigung von großen TypeScript-Monorepo-Builds und Dependency Management
Emily Parker
Product Engineer · Leapcell

Einleitung
In der sich ständig weiterentwickelnden Landschaft der Webentwicklung sind Monorepos zu einer immer beliebteren architektonischen Wahl für die Verwaltung komplexer Full-Stack-Anwendungen geworden. Sie bieten unbestreitbare Vorteile wie vereinfachtes Code-Sharing, konsistente Tooling-Sets und optimierte Deployments. Wenn diese Monorepos, insbesondere solche, die mit TypeScript erstellt wurden, jedoch wachsen, stoßen Entwickler häufig auf erhebliche Herausforderungen: langsame Build-Zeiten und kompliziertes Dependency Management. Eine große TypeScript-Codebasis mit ihrer statischen Typisierung und ihren Kompilierungsanforderungen, gepaart mit einer Full-Stack-Architektur, die oft mehrere voneinander abhängige Anwendungen und Bibliotheken umfasst, kann schnell zu einer Entwicklungserfahrung führen, die von langen Feedbackschleifen geplagt wird. Dies frustriert nicht nur die Entwickler, sondern behindert auch die Agilität und die Liefergeschwindigkeit. Dieser Artikel untersucht praktische Strategien und demonstriert verschiedene Werkzeuge, um diese Hürden zu überwinden und ein langsames, umständliches Monorepo in eine effiziente, angenehm zu nutzende Entwicklungsumgebung zu verwandeln.
Kernkonzepte verstehen
Bevor wir uns mit Optimierungstechniken befassen, wollen wir ein gemeinsames Verständnis der Schlüsselkonzepte aufbauen, die für diese Diskussion zentral sind.
Monorepo
Ein Monorepo ist ein einzelnes Repository, das mehrere, unterschiedliche Projekte enthält, oft mit zusammenhängendem Code. In einem Full-Stack-Kontext kann dies eine Frontend-Anwendung, eine Backend-API, gemeinsam genutzte UI-Komponenten und Utility-Bibliotheken umfassen, die alle im selben Git-Repository liegen.
TypeScript
TypeScript ist eine stark typisierte Obermenge von JavaScript, die zu einfachem JavaScript kompiliert wird. Während es die Codequalität und Wartbarkeit durch statische Typenprüfung erheblich verbessert, fügt dieser Kompilierungsschritt dem Build-Prozess Overhead hinzu, insbesondere bei großen Projekten.
Build-Geschwindigkeit
Dies bezieht sich auf die Zeit, die benötigt wird, um Quellcode (TypeScript, JSX usw.) in deploybare Artefakte (JavaScript, CSS, oft gebündelt und minifiziert) zu transformieren. In einem Monorepo beinhaltet der "Build" typischerweise die Kompilierung mehrerer Projekte und ihrer Abhängigkeiten.
Dependency Management
Dies umfasst, wie Pakete innerhalb des Monorepos miteinander in Beziehung stehen, wie externe Drittanbieterbibliotheken gehandhabt werden und wie sich diese Beziehungen auf den Build-Prozess auswirken. Werkzeuge wie npm, Yarn und pnpm werden dafür verwendet.
Die Säulen der Monorepo-Optimierung
Die Optimierung eines großen TypeScript Full-Stack-Monorepos dreht sich im Allgemeinen um mehrere Schlüsselprinzipien: Parallelität, Caching, inkrementelle Builds und effiziente Abhängigkeitsgraphen.
1. Arbeitende Workspaces für Dependency Graphing nutzen
Moderne Paketmanager bieten die "Workspaces"-Funktion, die für Monorepos entscheidend ist. Workspaces ermöglichen es Ihnen, mehrere Pakete innerhalb eines einzigen Root-Repositorys zu verwalten, interne Paketabhängigkeiten zu handhaben und gemeinsame externe Abhängigkeiten in das Root-node_modules
-Verzeichnis zu "hoisten", wodurch Speicher gespart und Installationszeiten verbessert werden.
Betrachten Sie eine Monorepo-Struktur:
my-monorepo/
├── packages/
│ ├── frontend/
│ │ ├── src/
│ │ └── package.json
│ ├── backend/
│ │ ├── src/
│ │ └── package.json
│ └── shared-ui/
│ ├── src/
│ └── package.json
└── package.json
Das Root-package.json
würde Workspaces definieren:
// my-monorepo/package.json { "name": "my-monorepo-root", "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "turbo run build" // Verwendung eines Monorepo-Tools wie TurboRepo } }
Jedes package.json
eines Pakets würde seine internen Abhängigkeiten deklarieren:
// my-monorepo/packages/frontend/package.json { "name": "frontend", "version": "1.0.0", "dependencies": { "shared-ui": "workspace:*" // Verweis auf ein internes Paket } }
Diese Einrichtung ermöglicht es Paketmanagern, einen genauen Abhängigkeitsgraphen zu erstellen, den Monorepo-Tools dann nutzen können.
2. Monorepo-fähige Build-Tools
Allgemeine Task-Runner wie npm run build
haben Schwierigkeiten mit den Abhängigkeiten zwischen Paketen und den Caching-Anforderungen eines Monorepos. Hier glänzen spezialisierte Monorepo-Tools. Werkzeuge wie Turborepo oder Nx sind darauf ausgelegt, den Abhängigkeitsgraphen Ihres Monorepos zu verstehen und Build-Prozesse zu optimieren.
Lassen Sie uns dies mit Turborepo veranschaulichen:
- Task-Graph: Turborepo erstellt einen Task-Graphen, der angibt, welche Tasks (z. B.
build
,test
,lint
) von welchen anderen abhängen. Zum Beispiel könntefrontend#build
vonshared-ui#build
abhängen. - Intelligentes Caching: Es hasht Dateiinhalte,
package.json
- undtsconfig.json
-Dateien und sogarnode_modules
für jeden Task. Wenn die Eingaben für einen Task seit dem letzten Lauf (lokal oder in einem Remote-Cache) nicht geändert wurden, überspringt Turborepo den Task und stellt seine Ausgaben aus dem Cache wieder her. - Parallele Ausführung: Tasks, die nicht voneinander abhängen, können parallel ausgeführt werden, was die Gesamt-Build-Zeit erheblich reduziert.
Beispiel: Turborepo-Konfiguration
Installieren Sie zuerst Turborepo in Ihrem Monorepo-Root: npm install turbo --save-dev
.
Definieren Sie dann Tasks in einer turbo.json
-Datei in Ihrem Root:
// turbo.json { "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], // Hängt vom "build" seiner Abhängigkeiten ab "outputs": ["dist/**", ".next/**"] // Diese Ausgaben cachen }, "test": { "dependsOn": ["^build"], "outputs": [] }, "lint": { "outputs": [] }, "dev": { "cache": false, // Dev-Server werden normalerweise nicht gecached "persistent": true // Läuft weiter } } }
Wenn Sie nun turbo run build
ausführen, tut Turborepo Folgendes:
- Analysiert den Abhängigkeitsgraphen für alle
build
-Skripte in Ihren Paketen. - Parallelisiert Builds, wo möglich.
- Casht Build-Ausgaben. Nachfolgende Läufe sind nahezu sofort da, wenn keine relevanten Änderungen vorgenommen wurden.
Stellen Sie sich eine Änderung in shared-ui
vor. Turborepo würde nur shared-ui
und frontend
(das von shared-ui
abhängt) neu erstellen und backend
unberührt lassen, selbst wenn Sie turbo run build
im gesamten Monorepo ausführen.
3. Inkrementelle TypeScript-Builds
TypeScript selbst unterstützt inkrementelle Kompilierung, bei der nur geänderte Dateien oder solche, deren Abhängigkeiten sich geändert haben, neu kompiliert werden. Dies wird über die Compiler-Option incremental
in tsconfig.json
aktiviert.
// tsconfig.json (oder tsconfig.build.json) { "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./.tsbuildinfo", // Wo Build-Infos gespeichert werden // ... andere Optionen } }
Wenn incremental
auf true
gesetzt ist, erstellt TypeScript eine .tsbuildinfo
-Datei, die Informationen über den Projektgraphen und den Build-Status speichert. Bei nachfolgenden Kompilierungen verwendet tsc
diese Datei, um schnell zu bestimmen, was neu kompiliert werden muss.
Obwohl hilfreich, kann tsc --build --watch
oder einfach tsc --build
in einem Monorepo immer noch mehr neu erstellen als nötig über Pakete hinweg, wenn es nicht von einem Monorepo-Tool koordiniert wird. Turborepo und Nx umschließen typischerweise tsc
-Aufrufe, sodass ihre Caching-Mechanismen in Verbindung mit den inkrementellen Funktionen von TypeScript arbeiten.
4. Optimierte TypeScript-Konfiguration
Das Feinabstimmen Ihrer tsconfig.json
-Dateien kann Leistungsvorteile bringen.
noEmit
undemitDeclarationOnly
: Für Pakete, die reine Typdefinitionen sind (z. B. ein gemeinsames Typenpaket), können SienoEmit: true
verwenden, wenn keine JS-Ausgabe benötigt wird, oderemitDeclarationOnly: true
, um nur.d.ts
-Dateien zu generieren, ohne den eigentlichen Runtime-JS zu kompilieren.references
: Die Project References-Funktion von TypeScript ermöglicht es Ihnen, Ihr großes Projekt in kleineretsconfig.json
-Dateien aufzuteilen. Dies ermöglicht estsc
, schnellere inkrementelle Überprüfungen durchzuführen, indem nur betroffene Projekte neu kompiliert werden. Dies integriert sich eng mit Monorepo-Tools.
// my-monorepo/packages/frontend/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { // ... }, "references": [ { "path": "../shared-ui" } // Verweis auf ein internes Paket ] }
Auf diese Weise weiß die tsconfig
von frontend
, wenn sich shared-ui
ändert, dass sie gegen die neuen shared-ui
-Typen typgeprüft werden muss.
5. Effiziente Paketmanager
Während npm und Yarn Classic funktionieren, bieten pnpm und Yarn Plug'n'Play (PnP) erhebliche Leistungsvorteile für Monorepos, hauptsächlich durch die Optimierung von node_modules
.
-
pnpm: Verwendet einen inhaltsadressierbaren Speicher, um Speicherplatz zu sparen und die Installation zu beschleunigen. Wenn mehrere Projekte dieselbe Version eines Pakets verwenden, speichert pnpm dieses Paket nur einmal auf der Festplatte und verknüpft es dann per Hardlink oder Symlink in die
node_modules
-Ordner der einzelnen Projekte. Dies machtnpm install
viel schneller und verbraucht weniger Speicherplatz.Um pnpm zu verwenden, ersetzen Sie einfach
npm install
durchpnpm install
in Ihrem Monorepo-Root. -
Yarn PnP: Löst das "Hoisting-Problem" von
node_modules
, indem eine.pnp.cjs
-Datei generiert wird, die Paketnamen ihren genauen Speicherorten zuordnet. Dies macht in vielen Fällen ein physischesnode_modules
-Verzeichnis überflüssig, was zu noch schnelleren Installationen und zuverlässigerer Abhängigkeitsauflösung führt.
6. Remote Caching
Für Teams ist lokales Caching gut, aber Remote Caching ist transformativ. Werkzeuge wie Turborepo können Cache-Artefakte in einen gemeinsamen Remote-Cache hoch- und herunterladen (z. B. Vercels Remote Cache, AWS S3 oder ein benutzerdefinierter HTTP-Server).
Das bedeutet, dass, wenn ein Teammitglied (oder eine CI/CD-Pipeining) ein Projekt kompiliert, die nachfolgenden Builds von jedem anderen im Team oder in der CI/CD auch von diesem Cache profitieren, selbst wenn sie von Grund auf neu beginnen. Dies kann die CI-Build-Zeiten von Minuten auf Sekunden reduzieren.
Die Konfiguration des Remote Caching mit Turborepo beinhaltet normalerweise die Einrichtung von Umgebungsvariablen oder einer ~/.config/turborepo/config.json
-Datei mit Anmeldeinformationen für Ihren Caching-Anbieter.
Fazit
Die Optimierung eines großen TypeScript Full-Stack-Monorepos erfordert einen vielschichtigen Ansatz, der intelligente Werkzeuge mit durchdachter Konfiguration kombiniert. Durch die Nutzung von Monorepo-fähigen Build-Tools wie Turborepo oder Nx, die Aktivierung der inkrementellen Kompilierung und Projektverweise von TypeScript, die Nutzung effizienter Paketmanager wie pnpm und die Einführung von Remote Caching können Entwicklungsteams die Build-Geschwindigkeiten dramatisch verbessern und das Dependency Management optimieren. Das Ergebnis ist eine produktivere Entwicklungserfahrung, schnellere Feedbackschleifen und ein Monorepo, das problemlos mit Ihren Projekten skaliert. Die Investition in diese Optimierungsstrategien verwandelt einen potenziellen Performance-Engpass in einen leistungsstarken Beschleuniger für Ihren Entwicklungs-Workflow.