eduardweb.
TypeScriptAvansat#performance#typescript#dx#monorepo

Cum am îmblânzit monorepourile în TypeScript: Project References și Composite

De Alexandru Matei, 21 iun. 2026 · 18 vizualizări · 2 like-uri

Postat 21 iun. 2026
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "references": [
    { "path": "../shared-utils" }
  ]
}

Salutare! Am văzut mulți colegi care încep un monorepo, trântesc un paths global în rădăcină și apoi se miră de ce VS Code le consumă 4GB de RAM și build-ul pe CI durează o veșnicie. Am pățit și eu asta acum vreo trei ani pe un proiect cu 12 pachete interne și am înțeles rapid că trebuie să existe o cale mai bună decât să aruncăm cu resurse în serverele de build.

Soluția nu e să mărești instanța de AWS, ci să înțelegi cum funcționează project references și flag-ul composite.

De ce "paths" simplu este o capcană în monorepo

Când pui "paths": { "@shared/*": ["packages/shared/*"] } în tsconfig-ul din rădăcină, îi spui compilatorului doar unde să caute fișierele la nivel de editor ca să nu-ți dea erori cu roșu. Problema reală apare la compilare. TypeScript va compila de la zero tot codul din @shared ca și cum ar fi parte integrantă din proiectul curent, ignorând complet faptul că acel pachet poate a fost deja compilat în pipeline.

Dacă ai trei aplicații care importă acel @shared, codul ăla se compilează inutil de trei ori. La un proiect de acum un an, abordarea asta ne-a dus timpul de build la peste 6 minute în GitHub Actions. Era extrem de frustrant pentru că așteptam după orice fix minor.

Salvarea: composite și project references

Ideea din spatele references este simplă: tratează fiecare pachet ca pe un proiect TypeScript independent, pre-compilat.

Fiecare sub-proiect (de exemplu, packages/shared) trebuie să aibă propriul tsconfig.json cu flag-ul "composite": true. Acest flag forțează TypeScript să genereze fișiere .d.ts (declarații de tipuri) și să salveze un cache de build sub forma unui fișier tsconfig.tsbuildinfo.

În aplicația principală care consumă acest pachet, în loc de mapări dubioase în paths, adaugi o referință directă în array-ul references. TypeScript devine inteligent: când compilezi aplicația, se uită în folderul shared, vede că are deja definițiile de tipuri generate și le folosește direct pe acelea. Nu mai parsează din nou tot arborele de fișiere din shared.

Cifre reale și trade-offs

Am aplicat structura asta pe un monorepo care conținea un API în NestJS, două aplicații Next.js și vreo 4 pachete de utilitare și tipuri partajate:

  • Înainte: Build-ul complet pe CI dura în medie 4 minute și 12 secunde.
  • După configurare: Timpul a scăzut la 1 minut și 25 de secunde (o economie de aproape 65% la build time).
  • La cald (incremental build local): Sub 3 secunde după o modificare minoră în shared.

Dar vine și cu un cost, pentru că nimic nu e gratis în meseria asta. Trade-off-ul principal este complexitatea de configurare și mentenanța. Trebuie să menții manual ordinea dependențelor. Dacă modifici ceva în shared și nu rulezi tsc -b (build mode), aplicația de deasupra nu va vedea modificările de tipuri în timp real, pentru că ea citește din folderul dist al pachetului shared, nu direct din surse.

De asemenea, e obligatoriu să folosești comanda tsc --build (sau tsc -b) în loc de simplul tsc. Doar flag-ul -b știe să citească corect graficul de dependențe și să compileze doar modulele care chiar s-au modificat.

Voi cum gestionați asta în echipele voastre? Mergeți pe configurarea nativă din TS sau lăsați tool-uri ca Turborepo sau Nx să se ocupe de toată magia asta sub capotă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.