tsrs vs tsgo: Cloudflare dice 8×, ma il benchmark arriva a 16,87×. Perché?
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 storico | tsgo | tsrs | checker | Fattore riportato |
|---|---|---|---|---|
| mui-docs, predefinito | 12.66 s | 0.75 s | 4 vs 16 | 16.87× |
mui-docs, entrambi --checkers 4 | 12.69 s | 1.07 s | 4 vs 4 | 11.81× |
mui-docs, entrambi --checkers 16 | 12.36 s | 0.77 s | 16 vs 16 | 16.07× |
| vscode, predefinito | 11.10 s | 0.82 s | 4 vs 16 | 13.55× |
vscode, entrambi --checkers 16 | 6.73 s | 0.81 s | 16 vs 16 | 8.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:
| Caso | src/test.ts completo | Risultato 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