tsgo e tsrs eseguono quattro casi sintetici TypeScript con la stessa revisione sorgente; diagnosi e codici di uscita coincidono in tutti i casi.
Figura: confronto diagnostico su quattro casi sintetici a file singolo. Tutti i risultati coincidono; non è un benchmark di compatibilità o prestazioni di un progetto.

Cloudflare ha introdotto tsrs parlando di una velocità fino a otto volte superiore a TypeScript 7.0 e di circa il 70% di memoria in meno. Eppure, aprendo il vecchio benchmark fissato a un commit di tsrs, ho trovato 16,87× per mui-docs e 13,55× per vscode con le impostazioni predefinite. Mi sono chiesto come si conciliassero con gli 8× dell’annuncio.

Non ho rieseguito il benchmark prestazionale su grandi progetti. Ho fatto una prova più piccola: ho fissato versioni corrispondenti di tsrs e tsgo (l’implementazione Go di TypeScript), eseguendole in un ambiente GitHub temporaneo e isolato su quattro semplici esempi TypeScript. Tutti e quattro i diagnostici coincidevano. È un buon punto di partenza, non una ragione per sostituire subito il type checker nella CI di produzione.

Perché conta il numero di checker nel benchmark

L’annuncio di Cloudflare riassume i primi risultati come fino a 8× più veloce e circa il 70% di memoria in meno. Il report storico di tsrs fissato a un commit mostra invece questi valori:

Benchmark storicotsgotsrscheckerFattore riportato
mui-docs, predefinito12.66 s0.75 s4 vs 1616.87×
mui-docs, entrambi --checkers 412.69 s1.07 s4 vs 411.81×
mui-docs, entrambi --checkers 1612.36 s0.77 s16 vs 1616.07×
vscode, predefinito11.10 s0.82 s4 vs 1613.55×
vscode, entrambi --checkers 166.73 s0.81 s16 vs 168.32×

Le righe predefinite non usano neanche lo stesso numero di checker: quattro per tsgo e sedici per tsrs. Anche quando entrambi ne usano quattro o sedici, però, i fattori superano 8×. Bisogna distinguere due affermazioni: l’annuncio sintetizza risultati iniziali; RESULTS.md è un’altra fotografia storica con versioni e condizioni precise. Otto non è il massimo della tabella e sedici non è una promessa per ogni progetto.

Non voglio inventare una causa guardando solo i numeri. Il report documenta una macchina Depot CI depot-ubuntu-24.04-16 (16 vCPU, 63 GB RAM, Linux x86_64), misure alternate e mediane per durata e picco di memoria. tsgo usa come base typescript@7.0.2, mentre tsrs porta un commit TypeScript 7.1-dev più recente ed è una build release ottimizzata con PGO. Lo stesso numero di checker non equivale alle stesse revisioni o modalità di compilazione.

Anche la memoria cambia in base a progetto e configurazione: per mui-docs il report indica 4,61× meno memoria con i valori predefiniti e 8,23× meno con sedici checker per entrambi. Il «70% in meno» è un riepilogo dei primi test, non una garanzia generale. Ho verificato i dati nella fonte upstream, senza ripetere le misure sui progetti grandi.

Cosa hanno davvero verificato i quattro casi

Ho confrontato tsrs@0.18.0-ts7.1.0-dev.20260929 con l’implementazione Go distribuita in typescript@7.1.0-dev.20260930.4. Entrambi corrispondono allo stesso commit sorgente b85298b6.

Nel preparare le dipendenze ho commesso un errore concreto: ho scambiato @typescript/typescript, il nome di workspace del repository sorgente, per un pacchetto pubblico npm. npm ha restituito E404 e il compilatore non era ancora partito. Il pacchetto root distribuito è typescript. In questa nightly la CLI si chiama ancora tsc, e @typescript/typescript-linux-x64 fornisce il compilatore Go nativo per Linux x64. Corretto il nome, sono arrivato all’esecuzione effettiva.

I quattro esempi condividono un unico tsconfig.json. Ho impostato types: [] per evitare l’inclusione automatica di pacchetti globali @types dall’ambiente, e skipLibCheck: false per mantenere il controllo dei file di dichiarazione. Ecco la configurazione realmente usata, invece di dire soltanto «strict mode»:

{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "noEmit": true,
    "skipLibCheck": false,
    "types": []
  },
  "include": ["src/*.ts"]
}

In ogni directory c’è un solo file src/test.ts:

Casosrc/test.ts completoRisultato tsgo / tsrs
valid (Tipi corretti)export const answer: number = 42;0 diagnostici, uscite 0 / 0
type-error (Assegnazione errata)export const answer: number = "wrong";TS2322 · uscite 2 / 2
module-missing (Modulo inesistente)import {missing} from "./unavailable.js"; export const answer = missing;TS2307 · uscite 2 / 2
generic-constraint (Vincolo generico)type OnlyText<T extends string> = {value:T}; export type Bad = OnlyText<number>;TS2344 · uscite 2 / 2

Ho usato --noEmit --pretty false --checkers 4 con entrambi i compilatori, confrontando percorso, riga e colonna, codice e messaggio, ordine dei diagnostici e codice d’uscita. Tutti e quattro i casi erano identici. Ma un solo file sorgente per caso non mette realmente alla prova quattro checker in parallelo; qui l’opzione serve soprattutto a fissare una condizione.

Lo stesso numero di errori non significa lo stesso diagnostico

La tabella di compatibilità di questa versione indica 13.458 baseline degli errori coincidenti su 13.462 e tutte le 12.779 baseline di .types e .symbols. Tuttavia il totale può trarre in inganno.

Nel report, drizzle-orm è un controesempio: con sedici checker su entrambi i lati si ottengono 10.846 errori per ciascuno, ma le posizioni diagnostiche differiscono. Il report lo segnala come bug di tsrs. Non ho riprodotto quel caso in prima persona: è un dato della fonte fissata. In CI e nella code review serve sapere se l’errore indica il file e la riga corretti, non soltanto se i conteggi coincidono.

Ci sono poi limiti pratici: questa versione non supporta ancora --init o --showConfig, e --locale non traduce i messaggi. I content mapper per .astro, .vue e .svelte funzionano in CLI/build ma non nel language server. I pacchetti precompilati non coprono Windows o macOS Intel. Ho testato solo la CLI su Linux in un ambiente GitHub isolato, non quelle piattaforme, i plugin dell’editor o LSP. Se usi astro check o i diagnostici in tempo reale, leggi prima la tabella di compatibilità per la tua versione. Ho anche una nota su Astro e TypeScript 7 per chi sta gestendo un aggiornamento.

Come li confronterei in un mio progetto

Inizierei da un worktree usa e getta o da un branch separato, senza toccare direttamente la directory di lavoro quotidiana. Se la prova non serve, posso scartarla.

Il seguente script evita un errore facile: due programmi possono terminare con lo stesso codice ma produrre diagnostici differenti. set +e è intenzionale. Alcuni esempi contengono errori e devono uscire con un codice non zero; voglio far terminare entrambi i compilatori, registrare gli stati e confrontare solo dopo gli output. Qui tsc è il comando Go della nightly fissata e /tmp/tsgo.out è semplicemente il nome del log.

pnpm add -D --save-exact tsrs@0.18.0-ts7.1.0-dev.20260929 typescript@7.1.0-dev.20260930.4

set +e
pnpm exec tsc -p . --noEmit --pretty false --checkers 4 > /tmp/tsgo.out 2>&1
tsgo_status=$?
pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 > /tmp/tsrs.out 2>&1
tsrs_status=$?
diff -u /tmp/tsgo.out /tmp/tsrs.out
diff_status=$?
set -e
printf 'tsgo_exit=%s tsrs_exit=%s diff_exit=%s\n' "$tsgo_status" "$tsrs_status" "$diff_status"
if [ "$tsgo_status" -ne "$tsrs_status" ] || [ "$diff_status" -ne 0 ]; then
  echo 'DIAGNOSTIC_DIFF_OR_EXIT_MISMATCH: inspect /tmp/tsgo.out and /tmp/tsrs.out'
  exit 1
fi

Se il primo confronto dà risultati diversi, salvo prima gli output originali. Poi provo separatamente --checkerAssignment go (assegnazione dei file e cronologia checker come tsgo) oppure --noLazyMembers (disattivazione dell’ottimizzazione lazy-member di tsrs). Abilitarli fin dall’inizio potrebbe nascondere le differenze del comportamento predefinito. Le opzioni sono spiegate nella guida e nella documentazione sulle impostazioni.

Questo script confronta i diagnostici, non misura tempo o memoria. Stavolta non ho completato una campagna di misurazione delle prestazioni. Per annotare un primo valore su Linux si può usare GNU time per durata e picco RSS; su macOS installa gnu-time e sostituisci la prima riga con TIME_BIN=gtime.

TIME_BIN=/usr/bin/time

set +e
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsgo-bench.metrics \
  pnpm exec tsc -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsgo-bench.out 2>&1
tsgo_status=$?
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsrs-bench.metrics \
  pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsrs-bench.out 2>&1
tsrs_status=$?
set -e

printf 'tsgo_exit=%s ' "$tsgo_status"; cat /tmp/tsgo-bench.metrics
printf 'tsrs_exit=%s ' "$tsrs_status"; cat /tmp/tsrs-bench.metrics

La coppia di risultati ricavata da quei comandi non basta per un benchmark: il carico in background può alterarla. Su uno stesso runner farei almeno cinque coppie alternando l’ordine, con lockfile, versioni, tsconfig e checker fissi, e confronterei le mediane e i massimi RSS. Separerei avvio a freddo e cache calda. Non ho svuotato la page cache del sistema operativo, quindi non lo chiamerei un test con cache di sistema completamente fredda. GNU time mostra di solito %M in KiB: verifica le unità prima di confrontare sistemi diversi.

All’inizio aggiungerei tsrs alla CI come lavoro supplementare che non blocca le modifiche, conservando tsc e i controlli del framework come verifiche obbligatorie. Solo dopo avere chiarito le divergenze, ottenuto misure stabili, attraversato un vero ciclo PR/rilascio e verificato il rollback valuterei la sostituzione. Questi quattro piccoli esempi dimostrano soltanto che, con quegli input, i due compilatori hanno emesso gli stessi diagnostici.

Data del test: 22 ottobre 2026