# Performance regressions in CI: the check that costs 70 ms

Seventy build files, 172 KB of compressed JavaScript and 36 KB of CSS: the budget check reads them in 61-77 ms and blocks the merge when the weight grows.

> Page: https://ui.eneawork.it/blog/performance-regressioni-ci
> Markdown: https://ui.eneawork.it/blog/performance-regressioni-ci.md

*2026-11-01*

Una regressione di performance si blocca in CI con un budget sul peso degli asset, non con un test Lighthouse a ogni push. Su 70 file di build il controllo legge tutto in 61-77 ms e fallisce se il JavaScript compresso supera la soglia. La build attuale misura 172 KB di JavaScript gzip e 36 KB di CSS.

Script Node eseguito tre volte su client/dist di questo sito, Windows, il 1 novembre 2026: 70 file, di cui 48 JavaScript e 17 CSS. JavaScript: 511 KB non compressi, 172 KB dopo gzip. CSS: 36 KB dopo gzip. Chunk più pesante: chunk-vendor con 141.671 byte non compressi e 45.376 byte compressi (44 KB); secondo, index con 44.281 byte compressi. Tempo del controllo: 77, 61 e 63 ms. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026. Soglie: web.dev, CC BY 4.0.

## Il budget, con i numeri di oggi

Il budget su 200 KB lascia 28 KB di margine, cioè circa un componente pesante o due piccoli. Non è una soglia arbitraria: è il numero misurato più il margine che vuoi concedere alla prossima feature. Un budget senza margine si rompe alla prima settimana e viene disattivato.

## Il controllo

const fs = require("fs"), zlib = require("zlib"), path = require("path"); const walk = (d, acc = []) => { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const f = path.join(d, e.name); e.isDirectory() ? walk(f, acc) : acc.push(f); } return acc; }; let js = 0, worst = 0; for (const f of walk("client/dist")) { const gz = zlib.gzipSync(fs.readFileSync(f)).length; if (f.endsWith(".js")) { js += gz; worst = Math.max(worst, gz); } } console.log(Math.round(js / 1024) + " KB JS gzip, chunk peggiore " + Math.round(worst / 1024) + " KB"); if (js > 200 * 1024 || worst > 50 * 1024) process.exit(1);

Lo script richiede solo Node e la cartella client/dist già costruita: nessuna dipendenza, nessun browser. Esce con codice 1 quando supera una soglia, e in una pipeline questo ferma il merge. Mettilo dopo il passo di build e prima del deploy: un budget che gira sul codice vecchio non serve.

## Cosa il budget non vede

Il peso non è il tempo: un chunk da 44 KB può diventare tre volte più lento senza cambiare dimensione, se cambia il lavoro che fa al primo render. Il budget intercetta la causa più comune, la dipendenza in più, e lascia passare la regressione più insidiosa, quella algoritmica. Per quella servono i dati di campo o una traccia in CI, che costano minuti invece di millisecondi.

## Il limite da dichiarare

I 172 KB sono la somma di tutti i chunk, non quello che una singola pagina scarica: il caricamento reale dipende dal routing e dal code splitting. Un budget per rotta è più preciso e più difficile da mantenere, perché richiede di sapere quali chunk carica ogni pagina. In più gzip non è brotli: con brotli i numeri assoluti scendono, il confronto tra due build resta valido.

## Dove continuare

Il catalogo EneUI indicizza 111.985 componenti, 1.556 blocchi e il record splite, un componente che si installa con le sue dipendenze e si vede subito nel budget. Per le soglie in pipeline leggi Lighthouse CI con soglie, per il peso delle librerie UI bundle size delle UI library, per caricare meno codice code splitting per route, e per il confronto tra architetture framework frontend a confronto.

## FAQ

### Meglio un budget sul peso o Lighthouse CI?

Il budget sul peso costa millisecondi e intercetta la causa più comune, una dipendenza in più. Lighthouse CI costa minuti e vede anche il tempo, ma è rumoroso su runner condivisi. Insieme coprono cause diverse.

### Quanto margine lascio sopra il valore misurato?

Tra il 10% e il 20%, abbastanza per una feature piccola e non abbastanza per una libreria intera. Un budget senza margine fallisce su ogni modifica e viene disattivato.

### Il budget deve essere per file o per pagina?

Per pagina è più preciso, perché misura quello che l'utente scarica. Per file è più semplice e stabile: si calcola sulla build senza sapere quali chunk carica il routing.

### Gzip o brotli nel budget?

Quello che serve davvero ai client. Brotli comprime meglio e dà numeri più bassi; l'importante è usare lo stesso algoritmo per la soglia e per la misura, altrimenti il confronto tra build non vale.

### Come evito che il budget blocchi un hotfix urgente?

Con un'eccezione esplicita e registrata, non disattivando il controllo. Alza la soglia in una pull request a parte, con la data e il motivo, e riportala giù dopo il rilascio.
