# Build più veloci: migrare al compilatore nativo di TypeScript 7 e a Rolldown

> Strategie di migrazione a TypeScript 7 e Rolldown in un monorepo: breaking change, --checkers e --builders, Vite 8, tsdown e benchmark affidabili in CI/CD.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-10-09
- URL: https://diegobetto.com/build-veloci-typescript-7-rolldown/
- Tag: typescript, javascript, vite, rolldown, ci-cd

Per dieci anni la toolchain JavaScript è stata scritta, in gran parte, in JavaScript. Funzionava, ma su un monorepo con qualche centinaio di migliaia di righe il type-check in CI durava minuti e la build di produzione altrettanti. Nel 2026 i due pezzi più lenti della pipeline sono stati sostituiti da motori nativi: il **compilatore di TypeScript 7**, portato in Go, e **Rolldown**, il bundler in Rust che da Vite 8 è il default. Entrambi promettono build da 8 a 10 volte più rapide, e in certi casi anche di più.

Le promesse, però, si misurano. In questo articolo vediamo come migrare un monorepo in modo incrementale: come gestire i breaking change di default, come configurare il parallelismo con `--checkers` e `--builders` senza saturare i runner, e come costruire un benchmark in CI che dica davvero quanto hai guadagnato in tempo e in memoria.

### Prima di tutto: separare type-check e build

Il guadagno più grande non arriva da uno strumento, ma da una decisione architetturale che molti progetti hanno già preso a metà: **chi controlla i tipi non è chi produce il JavaScript**.

| Compito                         | Strumento nel 2026                       | Cosa fa                                            |
| ------------------------------- | ---------------------------------------- | -------------------------------------------------- |
| Type-check                      | `tsc` (TypeScript 7, Go)                 | Controlla i tipi, `--noEmit`                       |
| Transpilazione e bundling app   | Vite 8 con Rolldown + Oxc (Rust)         | Rimuove i tipi file per file, bundle, minificazione |
| Build di librerie + `.d.ts`     | tsdown (Rolldown) o `tsc --build`        | JS per il pacchetto e dichiarazioni di tipo        |

Il bundler non legge i tipi: Oxc li rimuove file per file, senza conoscere il resto del programma. Per questo i due passaggi possono girare **in parallelo** come job separati, e un errore di tipo non deve bloccare il feedback sulla build (e viceversa). Se il tuo script di build oggi è `tsc && vite build`, questo è il primo cambiamento da fare, prima ancora di aggiornare le versioni.

```json
// package.json di un'app
{
  "scripts": {
    "typecheck": "tsc --noEmit --checkers 4",
    "build": "vite build"
  }
}
```

### Passo 0: misura il punto di partenza

Senza una baseline non saprai mai se la migrazione è servita, né a cosa attribuire una regressione. Misura tre cose, separatamente, sulla stessa macchina:

- **tempo di type-check** a freddo;
- **tempo di build** a freddo (senza cache di Turborepo/Nx, senza `node_modules/.vite`);
- **picco di memoria** di ciascun processo.

[hyperfine](https://github.com/sharkdp/hyperfine) è lo strumento giusto per i tempi, perché ripete il comando e riporta media e deviazione standard:

```bash
hyperfine --warmup 1 --runs 5 \
  --prepare 'rm -rf dist node_modules/.vite' \
  'npx vite build'
```

Per la memoria, `/usr/bin/time -v` (GNU time, su Linux) riporta il _Maximum resident set size_:

```bash
/usr/bin/time -v npx tsc --noEmit 2>&1 | grep "Maximum resident"
```

> **Misura a freddo e a caldo**
>
> In un monorepo con cache remota (Turborepo, Nx) la maggior parte delle build in CI è "a caldo":
> i pacchetti non toccati vengono ripristinati dalla cache. Il guadagno dei tool nativi pesa
> soprattutto sui cache miss — una PR che tocca un pacchetto alla base del grafo, o il primo build
> su un branch nuovo. Misura entrambi i casi, o rischi di sottovalutare (o sopravvalutare) il
> risultato.

### Parte 1: TypeScript 7

TypeScript 7 è un **porting**, non una riscrittura: stesso checker, stessa semantica dei tipi di 6.0. Quello che cambia sono i default di `tsconfig.json`, le opzioni rimosse e il parallelismo. I dettagli sono nell'[approfondimento su TypeScript 7](https://diegobetto.com/typescript-7-approfondimento/); qui ci concentriamo su come applicarli a un monorepo senza fermare il team.

#### Strategia: prima 6.0, poi 7.0

TypeScript 6.0 esiste proprio come ponte: segnala come deprecazioni tutto quello che in 7.0 diventa un errore. La sequenza più sicura è:

1. aggiornare tutto il monorepo a **6.0** e azzerare i warning di deprecazione;
2. attivare in 6.0 le opzioni che in 7.0 diventano default (`stableTypeOrdering`, `types` esplicito, `rootDir` esplicito);
3. solo a quel punto passare a **7.0**, che a quel punto dovrebbe richiedere poche modifiche.

#### I breaking change di default, in un monorepo

Nel singolo progetto i nuovi default si sistemano in dieci minuti. In un monorepo il problema è che ogni pacchetto ha il suo `tsconfig.json`, spesso ereditato da una base comune. Conviene fissare tutto **nella base**, in modo esplicito, così la migrazione diventa una modifica in un file solo:

```json
// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2024",
    "module": "esnext",
    "moduleResolution": "bundler",
    "types": [],
    "noUncheckedSideEffectImports": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}
```

```json
// packages/api/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "types": ["node"]
  },
  "include": ["src"]
}
```

I tre punti che rompono più spesso:

- **`types: []`**: i pacchetti Node perdono `process` e `Buffer`, quelli di test perdono `describe` e `expect`. Ogni pacchetto dichiara i suoi globali.
- **`rootDir`**: dove non è esplicito, la struttura di `dist/` può cambiare (`dist/src/index.js` invece di `dist/index.js`) e rompere gli `exports` di `package.json`. Va controllato pacchetto per pacchetto.
- **`baseUrl` rimosso**: gli alias vanno riscritti in `paths` relativi al `tsconfig.json` (`"@/*": ["./src/*"]`).

`isolatedModules` e `verbatimModuleSyntax` non sono richiesti da TypeScript 7, ma sono la garanzia che ogni file possa essere transpilato da solo — cioè che Oxc e Rolldown producano lo stesso risultato di `tsc`. Se separi type-check e build, attivali.

#### Il ponte per gli strumenti: TypeScript 6 in parallelo

TypeScript 7.0 non ha ancora un'API programmatica stabile: typescript-eslint, Volar (Vue), i plugin di Svelte, Astro e MDX continuano a importare `typescript` e hanno bisogno di 6.0 fino a 7.1. La soluzione è tenere entrambi i compilatori, con un alias npm per il pacchetto 6.0 (`@typescript/typescript6`) come descritto nell'[approfondimento](https://diegobetto.com/typescript-7-approfondimento/): il linter usa 6.0, il type-check in CI usa il binario nativo.

Avere entrambi installati ha un vantaggio collaterale: puoi confrontare i risultati. Prima di spegnere 6.0 in CI, per qualche giorno lancia entrambi e confronta le diagnostiche:

```bash
npx tsc6 -p packages/api --noEmit --pretty false | sort > ts6.txt
npx tsc  -p packages/api --noEmit --pretty false --checkers 4 | sort > ts7.txt
diff ts6.txt ts7.txt
```

L'ordinamento serve perché l'ordine delle diagnostiche può cambiare con il parallelismo. Le differenze attese sono quelle dovute ai nuovi default e all'ordine stabile dei tipi nei messaggi (`string | number` contro `number | string`); qualunque altra differenza va capita prima di fidarsi del nuovo compilatore.

#### `--checkers` e `--builders` senza saturare la CPU

TypeScript 7 introduce due livelli di parallelismo:

- **`--checkers N`** (default 4): quanti type checker lavorano in parallelo sui file dello _stesso_ progetto.
- **`--builders N`**: quanti progetti referenziati vengono compilati contemporaneamente con `tsc --build`.

Il punto che i benchmark sintetici non mostrano è che in un monorepo questi due numeri si **moltiplicano**, e si moltiplicano anche con il parallelismo dell'orchestratore:

```
thread di type-check ≈ task paralleli di Turborepo/Nx × builders × checkers
```

Turborepo, per default, esegue fino a 10 task in parallelo. Dieci pacchetti che lanciano `tsc --checkers 4` sono 40 checker su un runner GitHub Actions standard con 4 vCPU e 16 GB di RAM: il risultato non è più veloce, è più lento e a rischio di OOM, perché ogni checker costruisce i propri tipi e la memoria cresce con il numero di checker.

Due strategie funzionano:

**A. Un solo `tsc --build` dalla radice**, lasciando a TypeScript la gestione del grafo:

```json
// tsconfig.json alla radice del monorepo
{
  "files": [],
  "references": [
    { "path": "packages/core" },
    { "path": "packages/ui" },
    { "path": "packages/api" },
    { "path": "apps/web" }
  ]
}
```

```bash
npx tsc --build --builders 2 --checkers 2
```

Con i project references i pacchetti devono avere `composite: true` ed emettere dichiarazioni (`emitDeclarationOnly`), ma ottieni l'incrementalità di `tsc --build`: i progetti non modificati non vengono ricontrollati.

**B. L'orchestratore gestisce il parallelismo**, e ogni `tsc` ne usa poco:

```bash
turbo run typecheck --concurrency=2
# con "typecheck": "tsc --noEmit --checkers 2" in ogni pacchetto
```

Questa strategia si sposa meglio con la cache remota di Turborepo/Nx, perché ogni pacchetto è un task cacheabile separato.

In entrambi i casi la regola è la stessa: **il prodotto dei livelli di parallelismo non deve superare di molto i core disponibili**. E fissa i valori negli script, non lasciarli ai default: oltre alle prestazioni, un numero diverso di checker può, in rari casi, produrre diagnostiche diverse, quindi locale e CI devono usare gli stessi valori.

> **GOMAXPROCS**
>
> Il compilatore nativo è un programma Go, quindi rispetta la variabile d'ambiente `GOMAXPROCS`,
> che limita il numero di thread del sistema operativo che eseguono codice Go in contemporanea. Le
> versioni recenti del runtime Go leggono già i limiti di CPU dei container (cgroup), ma su runner
> condivisi o con limiti configurati in modo insolito impostarla esplicitamente è un modo semplice
> per mettere un tetto all'uso di CPU di tutti i processi `tsc` di un job.

#### Dichiarazioni `.d.ts` senza type checker: `isolatedDeclarations`

Nei monorepo con molte librerie interne, generare i `.d.ts` è spesso la parte più lenta della build, perché richiede il type checker. Con `isolatedDeclarations: true`, TypeScript ti obbliga ad annotare esplicitamente i tipi esportati (valori di ritorno delle funzioni pubbliche, costanti esportate):

```ts
// ❌ con isolatedDeclarations: il tipo di ritorno va dedotto dal corpo
export function createClient(url: string) {
  return new ApiClient(url);
}

// ✅ il tipo è dichiarato, il .d.ts si genera leggendo solo questo file
export function createClient(url: string): ApiClient {
  return new ApiClient(url);
}
```

In cambio, le dichiarazioni si possono generare file per file, senza type checker, con Oxc. È quello che fa **tsdown**, il bundler per librerie basato su Rolldown (l'erede naturale di tsup):

```ts
// packages/core/tsdown.config.ts
import { defineConfig } from "tsdown";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: true, // con isolatedDeclarations attivo usa Oxc invece del compilatore
});
```

Il type-check resta compito di `tsc --noEmit`; la build della libreria diventa indipendente dal checker e molto più rapida.

### Parte 2: Rolldown con Vite 8

Vite 8, stabile da marzo 2026, sostituisce la coppia esbuild (dev) + Rollup (build) con un unico bundler, **Rolldown**, arrivato alla 1.0 stabile a maggio 2026. Le trasformazioni JS/TS e la minificazione passano a **Oxc**, il CSS si minifica con Lightning CSS. Il vantaggio non è solo la velocità: dev e build usano finalmente lo stesso motore, quindi sparisce un'intera classe di bug "funziona in dev, si rompe in build".

#### Migrazione in due passi

Per tutto ciò che va oltre un progetto banale, conviene separare il cambio di bundler dal cambio di versione di Vite:

**1. Su Vite 7, prova `rolldown-vite`** (il pacchetto che ha anticipato l'integrazione) come drop-in:

```json
{
  "devDependencies": {
    "vite": "npm:rolldown-vite@7.2.2"
  }
}
```

Se qualcosa si rompe qui, sai che dipende da Rolldown e non da altre novità di Vite 8.

**2. Passa a Vite 8** sostituendo l'alias con `"vite": "^8.0.0"` e applicando le rinomine della configurazione.

#### Le modifiche di configurazione principali

Vite 8 include un livello di compatibilità che converte automaticamente buona parte della vecchia configurazione, ma conviene migrare esplicitamente per non dipendere da opzioni deprecate:

| Vite 7                                   | Vite 8                                              |
| ---------------------------------------- | --------------------------------------------------- |
| `build.rollupOptions`                    | `build.rolldownOptions`                             |
| `worker.rollupOptions`                   | `worker.rolldownOptions`                            |
| `optimizeDeps.esbuildOptions`            | `optimizeDeps.rolldownOptions`                      |
| `esbuild` (transform JS/TS)              | `oxc`                                               |
| `esbuild.jsx: "automatic"`               | `oxc.jsx: { runtime: "automatic" }`                 |
| `output.manualChunks` (oggetto)          | rimosso → `output.codeSplitting`                    |
| `output.manualChunks` (funzione)         | deprecato → `output.codeSplitting`                  |
| minificazione JS con esbuild             | Oxc (default)                                       |
| minificazione CSS con esbuild            | Lightning CSS (default)                             |
| `transformWithEsbuild` (plugin)          | `transformWithOxc`                                  |

Il caso più comune in un'app reale è lo split dei vendor. Prima:

```ts
// vite.config.ts — Vite 7
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ["react", "react-dom"],
          charts: ["recharts"],
        },
      },
    },
  },
});
```

Dopo, con i gruppi di `codeSplitting`, che supportano priorità e soglie di dimensione:

```ts
// vite.config.ts — Vite 8
export default defineConfig({
  build: {
    rolldownOptions: {
      output: {
        codeSplitting: {
          groups: [
            { name: "react", test: /node_modules[\\/]react(-dom)?[\\/]/, priority: 20 },
            { name: "charts", test: /node_modules[\\/]recharts/, priority: 15 },
            { name: "vendor", test: /node_modules/, priority: 10 },
          ],
        },
      },
    },
  },
});
```

#### Cosa può rompersi

- **Interop CommonJS**: l'import `default` dai moduli CJS ora è gestito in modo coerente, ma diverso da prima. Se un `import x from "pacchetto-cjs"` restituisce l'oggetto sbagliato, `legacy.inconsistentCjsInterop: true` ripristina temporaneamente il vecchio comportamento mentre sistemi il codice.
- **Plugin che usano internals**: i plugin che si appoggiano a hook Rollup non supportati (`resolveImportMeta`, `renderDynamicImport`, `resolveFileUrl`, `shouldTransformCachedModule`) o che chiamano esbuild direttamente vanno verificati uno per uno. esbuild non è più una dipendenza di Vite: se un plugin ne ha bisogno, va installato come devDependency.
- **Decoratori**: Oxc non abbassa i decoratori nativi. Se li usi (o usi decoratori legacy con `experimentalDecorators`), serve `@rolldown/plugin-babel` o `@rollup/plugin-swc`.
- **Browser target**: il default sale a Chrome/Edge 111, Firefox 114, Safari 16.4. Se supporti browser più vecchi, imposta `build.target` esplicitamente.
- **Formati di output**: Rolldown non supporta `system` e `amd`.
- **Opzioni esbuild senza equivalente**: `esbuild.banner`/`footer` vanno reimplementati con un piccolo plugin, `mangleProps` e simili non sono supportati.

> **Verifica il bundle, non solo la build**
>
> Una build che termina senza errori non significa un bundle equivalente. Dopo la migrazione,
> confronta numero e dimensione dei chunk (per esempio con `rollup-plugin-visualizer`, compatibile
> con Rolldown), lancia i test end-to-end sul build di produzione e tieni d'occhio i
> [Core Web Vitals](https://diegobetto.com/core-web-vitals-lcp-cls-inp/): un chunk di vendor diviso in modo
> diverso può cambiare l'LCP più di quanto la build più veloce faccia risparmiare in CI.

### Parte 3: un benchmark che regge in CI

I benchmark dei release post sono fatti su codebase molto grandi e su macchine potenti. Quello che conta è il tuo monorepo sul tuo runner. Un workflow minimale per GitHub Actions, da lanciare manualmente sul branch di migrazione:

```yaml
# .github/workflows/build-benchmark.yml
name: build-benchmark
on: workflow_dispatch

jobs:
  bench:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: sudo apt-get update && sudo apt-get install -y hyperfine

      - name: Type-check (TS 6 vs TS 7)
        run: |
          hyperfine --warmup 1 --runs 5 --export-markdown typecheck.md \
            'npx tsc6 -b --force' \
            'npx tsc -b --force --builders 2 --checkers 2'

      - name: Peak memory
        run: |
          /usr/bin/time -v npx tsc6 -b --force 2>&1 | grep "Maximum resident" | tee -a memory.txt
          /usr/bin/time -v npx tsc  -b --force --builders 2 --checkers 2 2>&1 | grep "Maximum resident" | tee -a memory.txt

      - name: Build app (cold)
        run: |
          hyperfine --runs 3 --prepare 'rm -rf apps/web/dist apps/web/node_modules/.vite' \
            --export-markdown build.md 'npx turbo run build --filter=web --force'

      - run: cat typecheck.md build.md memory.txt >> "$GITHUB_STEP_SUMMARY"
```

Qualche accorgimento per numeri affidabili:

- **Stesso runner, stesso job.** I runner condivisi hanno varianza alta: confrontare un job di ieri con uno di oggi non dice molto. Esegui baseline e nuova versione nello stesso job, uno dopo l'altro.
- **`--force`** su `tsc -b` e su Turborepo/Nx, per escludere cache incrementali e remote.
- **Ripeti.** Cinque esecuzioni con hyperfine e la deviazione standard ti dicono se una differenza del 10% è reale o rumore.
- **Memoria, non solo tempo.** Alzare `--checkers` riduce il tempo e aumenta la RAM. Su runner piccoli il punto ottimo è spesso più basso del default.
- **Misura il tempo totale della pipeline**, non solo dei singoli comandi: se type-check e build ora girano in parallelo, il guadagno reale è il job più lungo, non la somma.
- **Prova più valori di parallelismo** sulla tua macchina tipica (per esempio `--checkers 1, 2, 4, 8`) prima di sceglierne uno: la curva si appiattisce presto, e oltre un certo punto i checker in più costano solo memoria.

Un'ultima nota sui numeri: se `tsc` prima durava 40 secondi e ora 5, ma l'intera pipeline passa da 9 a 7 minuti, il collo di bottiglia era altrove (installazione delle dipendenze, test, upload degli artefatti). È comunque un buon risultato, ma è il dato da comunicare al team.

### Checklist di migrazione

1. Separa type-check (`tsc --noEmit`) e build (bundler) in script e job distinti.
2. Misura la baseline: tempi a freddo e a caldo, picco di memoria.
3. Aggiorna a TypeScript 6.0 e azzera le deprecazioni.
4. Rendi espliciti in `tsconfig.base.json` `types`, `rootDir`, `module`, `moduleResolution`; rimuovi `baseUrl`.
5. Installa TypeScript 7 mantenendo `@typescript/typescript6` per linter e plugin; confronta le diagnostiche dei due compilatori.
6. Scegli una strategia di parallelismo (un solo `tsc --build` oppure orchestratore) e fissa `--checkers`/`--builders` negli script.
7. Valuta `isolatedDeclarations` e tsdown per le librerie interne.
8. Prova `rolldown-vite` su Vite 7, poi passa a Vite 8 e rinomina le opzioni (`rolldownOptions`, `oxc`, `codeSplitting`).
9. Verifica plugin, interop CJS, decoratori e browser target.
10. Confronta i bundle e lancia i test end-to-end sul build di produzione.
11. Ripeti il benchmark nello stesso job CI e documenta tempo, memoria e durata totale della pipeline.

### FAQ

> **Devo migrare TypeScript e Vite insieme?**
>
> No, ed è meglio di no. Sono due migrazioni indipendenti: TypeScript 7 cambia il type-check,
> Rolldown cambia il bundling. Farle in PR separate rende ogni regressione attribuibile a una sola
> causa.

> **Quanti checker devo usare in CI?**
>
> Dipende dal runner e da quanti task giri in parallelo. Su un runner da 4 vCPU con un solo `tsc
> --build`, il default di 4 è ragionevole; se l'orchestratore esegue già più task insieme, scendi a
> 1–2 checker per processo. Misura tempo e memoria con due o tre valori e fissa quello migliore.

> **Posso usare TypeScript 7 se uso typescript-eslint o Vue?**
>
> Sì per il type-check in CI, no (ancora) per gli strumenti che usano l'API del compilatore: fino a
> TypeScript 7.1 linter, Volar e i plugin dei framework devono usare 6.0 tramite il pacchetto
> `@typescript/typescript6`.

> **Vite 8 fa anche il type-check?**
>
> No, come le versioni precedenti: Oxc rimuove i tipi senza controllarli. Il type-check va eseguito
> con `tsc --noEmit` in uno script o job separato (o con `vite-plugin-checker` se lo vuoi durante lo
> sviluppo).

> **Rolldown funziona senza Vite?**
>
> Sì. Rolldown ha una propria CLI e un'API compatibile con quella di Rollup, ed è la base di tsdown
> per le librerie. Per le applicazioni, però, il modo più semplice di adottarlo resta Vite 8.

### Conclusioni

La migrazione ai tool nativi è una delle poche ottimizzazioni che non chiedono di riscrivere codice applicativo: il linguaggio resta lo stesso, il bundle dovrebbe restare equivalente, cambiano i motori. Il lavoro vero sta nei bordi — default di configurazione, plugin, parallelismo che si moltiplica tra livelli — ed è lì che una migrazione incrementale e un benchmark onesto fanno la differenza tra "abbiamo aggiornato le dipendenze" e "la CI ora impiega metà del tempo".

Se vuoi partire dalle novità del compilatore, leggi l'[introduzione a TypeScript 7](https://diegobetto.com/typescript-7-novita/); per i dettagli su ogni opzione rimossa, l'[approfondimento](https://diegobetto.com/typescript-7-approfondimento/).

**Riferimenti**

- [TypeScript Blog – annunci di TypeScript 7.0](https://devblogs.microsoft.com/typescript/)
- [TypeScript 7.1 Iteration Plan](https://github.com/microsoft/TypeScript/issues/63703)
- [Vite – Migrazione da v7](https://vite.dev/guide/migration)
- [Rolldown – opzione codeSplitting](https://rolldown.rs/reference/OutputOptions.codeSplitting)
- [tsdown – il bundler per librerie basato su Rolldown](https://tsdown.dev/)
- [TSConfig – isolatedDeclarations](https://www.typescriptlang.org/tsconfig/#isolatedDeclarations)
