eduardweb.
TypeScriptAvansat#performance#typescript#dx#monorepo

Cum am redus build time-ul cu 40% într-un monorepo folosind TS Project References

De Corina Dobre, 22 mai 2026 · 5 vizualizări · 3 like-uri

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

Dacă ai lucrat vreodată într-un monorepo cu mai mult de 3-4 pachete, știi deja că TypeScript poate deveni incredibil de leneș. Am avut un caz la fostul job: un proiect cu 12 sub-pachete și în jur de 85k linii de cod, unde build-ul dura aproape 3 minute doar pentru type-checking. Trecerea la project references și configurarea corectă ne-a salvat minute bune în CI și ne-a adus un boost de 40% la viteza de build local.

Capcana clasică: Doar paths în root

Cea mai comună greșeală pe care o văd este maparea tuturor pachetelor prin paths în tsconfig-ul din root. Pare simplu la început. Adaugi "@my-project/shared": ["packages/shared/src"] și totul compilează.

Dar există o problemă majoră cu abordarea asta: TypeScript tratează tot monorepo-ul ca pe un singur proiect uriaș. Când modifici o linie de cod în shared, compilerul reevaluează absolut tot. Dacă ai un serviciu mare care depinde de el, IDE-ul tău o să înceapă să gâfâie, iar consumul de RAM sare lejer de 4GB. Proprietatea paths e gândită pentru alias-uri interne dintr-un singur proiect, nu pentru legat pachete independente în monorepo.

Soluția curată: Composite și Project References

Pentru a rezolva asta, trebuie să spargem proiectul în module independente folosind flag-ul composite: true. Acest flag îi spune lui TS două lucruri:

  1. Proiectul va produce fișiere .d.ts (declarații de tipuri) pentru ca alte proiecte să le poată consuma direct, fără să recompileze tot codul sursă de fiecare dată.
  2. Va genera un fișier .tsbuildinfo care ține evidența cache-ului pentru build-uri incrementale.

Trade-off-ul sincer? Configurația devine mult mai rigidă. Nu mai poți face importuri circulare între pachete (ceea ce e un lucru bun pe termen lung, dar te obligă la refactoring acum) și trebuie să declari explicit fiecare dependență în array-ul de references. Dacă pachetul api folosește shared, trebuie să specifici asta clar în tsconfig.json-ul din api, altfel compilerul va refuza să ruleze.

Cum funcționează rularea incrementală

La root ai un tsconfig.json simplu care doar pointează către sub-proiecte, iar rularea nu se mai face cu tsc, ci cu tsc --build (sau prescurtat tsc -b).

Comanda asta e deșteaptă: analizează graful de dependențe, vede ce s-a schimbat de la ultimul build (folosind acele fișiere .tsbuildinfo) și compilează doar diferențele. Pe proiectul nostru de 85k linii de cod, după o modificare minoră în API, build-ul incremental dura sub 4 secunde, față de 45 de secunde cât dura înainte fără cache.

Singurul minus real e că în pipeline-urile de CI/CD trebuie să ai grijă să salvezi folderul de build cache sau să dai cache la fișierele .tsbuildinfo, altfel build-ul o ia de la capăt pe mașinile virtuale curate din cloud.

Voi cum gestionați asta? Mergeți pe project references sau lăsați bundlerul (Vite, Esbuild, Turbopack) să ignore tipurile la build și faceți type-checking global separat?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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