Migliorare le prestazioni del sito distribuendo più CSS
C'è un istinto radicato nel lavoro frontend moderno che dice che la pagina più veloce distribuisce il meno possibile di tutto: meno JavaScript, meno font, meno richieste. Quindi quando il team di ingegneria di GitHub ha scritto di migliorare le prestazioni del sito distribuendo più CSS, sembrava una contraddizione. La soluzione per un'interfaccia lenta e pesante di JavaScript non era quella di rimuovere lo styling dall'app. Era quella di rimetterne di più, sotto forma di fogli di stile semplici, statici e memorizzabili nella cache, ed eliminare il JavaScript che esisteva solo per generare stili nel browser.
La distinzione è l'intero articolo. Distribuire "più CSS" non significa un foglio di stile più grande che gonfia il tuo percorso critico. Significa spostare la generazione degli stili fuori dal runtime e dentro la build, poi lasciare che il browser faccia ciò in cui è davvero eccellente: analizzare un foglio di stile una volta, memorizzarlo nella cache e riutilizzarlo in ogni navigazione.
Il costo nascosto dello styling a runtime
Le librerie CSS-in-JS hanno risolto problemi reali. Hanno dato agli autori di componenti lo scoping, la co-locazione e la capacità di interpolare le props negli stili senza inventare una convenzione di denominazione. Il compromesso era architetturale: hanno spostato un problema di tempo di compilazione nel browser.
La generazione degli stili è lavoro JavaScript
Ogni pagina renderizzata dal server che utilizza una libreria CSS-in-JS a runtime chiede al browser di fare lo stesso lavoro due volte. Il server serializza gli stili in tag <style>, poi la libreria client si sveglia, percorre l'albero dei componenti, calcola di nuovo l'hash dei nomi delle classi e riconcilia il risultato. Quel lavoro avviene sul thread principale, in competizione con gli stessi passaggi di layout e paint che stai cercando di accelerare.
// Runtime CSS-in-JS: this stylesheet does not exist until hydration runs
const Button = styled.button`
background: ${(p) => (p.primary ? "#1f883d" : "transparent")};
border: 1px solid #d0d7de;
padding: 6px 16px;
border-radius: 6px;
font: inherit;
`;
Lo stesso componente espresso come foglio di stile in fase di build rimuove sia la dipendenza dal runtime sia il lavoro di serializzazione:
/* button.module.css, parsed once, cached forever */
.button {
padding: 6px 16px;
border: 1px solid #d0d7de;
border-radius: 6px;
font: inherit;
background: transparent;
}
.buttonPrimary {
background: #1f883d;
color: #ffffff;
}
import styles from "./button.module.css";
export function Button({ primary, ...props }) {
const className = primary
? `${styles.button} ${styles.buttonPrimary}`
: styles.button;
return <button className={className} {...props} />;
}
Niente qui è nuovo. Ciò che è facile sottovalutare è quanto di un bundle possiede silenziosamente uno stack CSS-in-JS maturo: la libreria stessa, le utilità di interpolazione delle props, il provider del tema, il serializzatore e il codice duplicato del framework che cresce quando ogni pacchetto dipende dalla propria copia.
Si trova direttamente sul percorso di rendering critico
Un <link rel="stylesheet"> blocca il rendering, ma lo blocca una volta sola, e il browser può analizzare il file mentre il flusso HTML sta ancora arrivando. Il CSS a runtime funziona diversamente: il browser non può nemmeno iniziare a costruire gli stili dei componenti finché il JavaScript non è stato scaricato, analizzato, compilato ed eseguito. Su un telefono di fascia media si tratta di un secondo o più di lavoro sul thread principale prima che un singolo pulsante riceva il suo colore di sfondo.
Frammenta la tua cache
I nomi delle classi generati cambiano ogni volta che il codice sorgente del componente cambia, il che significa che il foglio di stile si invalida quasi a ogni deploy. Dividilo tra dozzine di bundle a livello di route e ottieni un tasso di successo della cache che sembra sano in una dashboard ma raramente si attiva nelle visite ripetute. Un foglio di stile statico con hash del contenuto, al contrario, può essere servito con Cache-Control: public, max-age=31536000, immutable e non essere mai effettivamente scaricato due volte.
Raddoppia il lavoro durante l'idratazione
Le discrepanze di idratazione tra stili iniettati dal server e stili generati dal client sono una fonte classica di sfarfallio. Il browser dipinge un insieme di regole, poi la libreria client le sostituisce durante l'interazione, forzando un ricalcolo degli stili e talvolta uno spostamento del layout. I nomi delle classi statici calcolati in fase di build sono identici su entrambi i lati, quindi il primo paint è anche il paint finale.

Cosa ha effettivamente cambiato GitHub
La migrazione è meglio compresa come cinque mosse architetturali piuttosto che un singolo refactoring. Nessuna di esse richiede di abbandonare un modello a componenti, e nessuna richiede una riscrittura completa.

I token di design sono diventati proprietà personalizzate CSS
Le scale di colore, spaziatura, raggio e tipografia sono state spostate in un blocco globale :root come proprietà personalizzate. I componenti fanno riferimento a var(--color-bg-default) invece di importare un oggetto tema JavaScript. Questo è ciò che rende il CSS statico abbastanza espressivo da sostituire gli stili dinamici nella maggior parte dei casi, e consente la tematizzazione a runtime (modalità scura, alto contrasto) con zero JavaScript.
Stili dei componenti compilati in anticipo
Ogni componente ha ottenuto un vero foglio di stile, emesso in fase di build e con scope tramite un hash o una convenzione di modulo. L'output è CSS ordinario che qualsiasi browser può analizzare alla velocità di analisi.
CSS critico inlinato nell'head del documento
Solo le regole necessarie per il primo viewport vengono inlinate nell'HTML. Si tratta tipicamente di 8–14 KB gzip, e significa che il primo paint non aspetta mai un round trip di rete per il foglio di stile che dà forma alla pagina visibile.
<head>
<!-- Critical: exactly what the first viewport needs, nothing more -->
<style>
/* reset + tokens + header + hero + above-the-fold grid */
</style>
<!-- Everything else: one hashed, immutable, long-cached file -->
<link
rel="stylesheet"
href="/assets/app.8f3a1c2d.css"
media="print"
onload="this.media='all'"
/>
<noscript>
<link rel="stylesheet" href="/assets/app.8f3a1c2d.css" />
</noscript>
</head>
Il markup renderizzato dal server porta i nomi delle classi finali
Poiché i nomi delle classi esistono in fase di build, l'HTML che il server restituisce è già stilizzato. Non c'è un secondo passaggio, nessuna fase di riconciliazione e nessun flash di contenuto non stilizzato su connessioni lente.
I livelli a cascata hanno sostituito gli hack di specificità
Un sistema di design condiviso vive o muore su un ordine di cascata prevedibile. Dichiarare l'ordine dei livelli una volta, in anticipo, rende gli override deterministici e rimuove l'incentivo a ricorrere a !important o a selettori sempre più profondi.
@layer reset, tokens, base, components, utilities;
@layer components {
.card {
background: var(--color-canvas-default);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-2);
padding: var(--space-4);
}
}
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
}
I numeri: cosa cambia quando distribuisci più CSS
Spostare la generazione degli stili al tempo di build cambia la forma di ciò che distribuisci, non solo il volume. I byte che rimangono sono economici: si comprimono bene, sono condivisi tra ogni route e vengono memorizzati nella cache per un anno.

Il guadagno architetturale si manifesta in quattro punti contemporaneamente: meno JavaScript da scaricare, meno lavoro sul thread principale prima del primo paint, un foglio di stile memorizzabile nella cache condiviso tra le navigazioni e markup identico su server e client.
| Approach | JS shipped (gzip) | CSS shipped (gzip) | Runtime style work | Cacheable across deploys |
|---|---|---|---|---|
| Runtime CSS-in-JS | ~180 KB | ~12 KB | High | No |
| Single monolithic stylesheet | ~96 KB | ~88 KB | None | Yes |
| Inline critical + split static sheets | ~78 KB | ~54 KB | None | Yes |
| Utility-first CSS | ~85 KB | ~40 KB | None | Yes |
Il foglio di stile più veloce è quello che il browser analizza una volta, memorizza nella cache per un anno e non deve mai più generare.
Un manuale pratico di migrazione
Non hai bisogno del codebase di GitHub per applicare lo stesso ragionamento. La migrazione è incrementale e ogni passo è distribuibile in modo indipendente.

1. Inventaria ciò che effettivamente viene renderizzato a runtime
Apri DevTools, carica una pagina e conta document.styleSheets. Se trovi dozzine di tag <style> iniettati con nomi di classi con hash, hai trovato il tuo obiettivo. Fai un confronto incrociato con il pannello Performance: cerca attività lunghe tra DOMContentLoaded e il first contentful paint.
2. Estrai il CSS above-the-fold
Inizia con il blocco inline più piccolo possibile. Un'euristica affidabile è inlinare reset, token e le regole utilizzate dalle prime due altezze del viewport. Tutto ciò che è sotto la piega viene caricato dal foglio di stile differito.
3. Converti i componenti a runtime in classi statiche
Lavora componente per componente, iniziando da quelli con il maggior traffico. Le varianti guidate dalle props diventano classi modificatrici o proprietà personalizzate impostate su un wrapper. Mantieni l'API del componente identica in modo che i punti di chiamata non cambino.
// Genuinely dynamic values still belong in an inline style, not a generated sheet
<div
className={styles.progressBar}
style={{ "--progress": `${percent}%` }}
role="progressbar"
aria-valuenow={percent}
/>
.progressBar {
width: var(--progress, 0%);
transition: width 200ms ease-out;
}
Questo pattern è il cavallo di battaglia dell'intera migrazione: la struttura dello stile è statica e memorizzabile nella cache, mentre il valore rimane dinamico e non costa nulla aggiornarlo.
4. Stabilisci presto l'ordine dei livelli
Dichiara l'ordine di @layer all'inizio del tuo foglio di stile di ingresso prima di avere un problema di specificità, non dopo. Aggiungere a posteriori i livelli a una cascata legacy è molto più doloroso che iniziare con essi.
5. Applica l'hash ai nomi dei file e memorizza nella cache in modo immutabileIl content hashing combinato con immutable è ciò che trasforma "più CSS" in "CSS gratuito". La seconda visualizzazione di una pagina in una sessione dovrebbe essere servita dalla cache HTTP per ogni regola di stile del sito.
6. Mantieni lo styling dinamico per ciò che è veramente dinamico
I valori basati su transform, posizione di scorrimento e misurazioni appartengono agli stili inline, a requestAnimationFrame o alla Web Animations API. Non reintrodurre un motore di stile runtime per risolvere un problema che le proprietà personalizzate CSS risolvono già.
Come misurare il miglioramento
Misura le stesse tre cose prima e dopo: paint timing, long task del thread principale e conteggio dei fogli di stile. Un piccolo script PerformanceObserver ti fornisce tutte e tre senza aggiungere una dipendenza di monitoraggio.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, Math.round(entry.startTime));
}
}).observe({ type: "paint", buffered: true });
new PerformanceObserver((list) => {
const long = list.getEntries().filter((e) => e.duration > 50);
console.log(`${long.length} long tasks after load`);
}).observe({ type: "longtask", buffered: true });
console.log("stylesheets:", document.styleSheets.length);
Aspettati il maggiore miglioramento nel time-to-first-paint nelle visite ripetute e nei profili mobili limitati, dove domina il costo di parsing JavaScript eliminato. Il Largest Contentful Paint tende a migliorare più gradualmente, release dopo release.

Insidie da evitare
L'approccio non è esente da rischi, e la maggior parte dei fallimenti deriva dall'applicarlo in modo troppo aggressivo.

- **CSS critico incompleto.**Se sbagli a indovinare il primo viewport, ottieni un reflow visibile all'arrivo del foglio di stile differito. Genera il blocco critico automaticamente da un render reale, non a mano. -**Inline di tutto.**Inlineare l'intero foglio di stile in ogni risposta HTML vanifica la cache e gonfia la dimensione del documento. Fai l'inline del minimo indispensabile, differisci il resto. -**Istinto del bundle totale.**Il vecchio consiglio di concatenare tutto il CSS in un unico file gigante è precedente a HTTP/2. Suddividere per route è spesso meglio ora, purché le primitive condivise siano memorizzate nella cache separatamente. -Mescolare due sistemi. Eseguire CSS-in-JS runtime e fogli di stile statici fianco a fianco per troppo tempo raddoppia la superficie di stile e rende il debugging della cascata miserabile. Imposta una scadenza di migrazione per directory di componenti.
La trappola non tecnica più comune è trattare questo come una riscrittura big-bang. I team che hanno successo convertono una manciata di componenti ad alto traffico, misurano il delta e usano quei dati per giustificare il lotto successivo.
Usare DivMagic per fare reverse engineering di un'architettura CSS reale
Una parte sottovalutata di questa migrazione è vedere come i siti di produzione maturi strutturano effettivamente i loro fogli di stile. Quando ispezioni un sito che distribuisce bene il CSS statico, puoi imparare di più in cinque minuti che in un'ora di lettura della documentazione: come stratificano la cascata, come delimitano i token, come separano il critico dal differito e come nominano i modificatori.
DivMagic è costruito esattamente per quel flusso di lavoro. Invece di copiare a mano gli stili degli elementi da DevTools una regola alla volta, selezioni qualsiasi UI su qualsiasi pagina e importi l'HTML e il CSS risultanti direttamente nel tuo editor, pronti per essere adattati. Questo è genuinamente utile in due situazioni: prototipare un componente da un design di riferimento e studiare come un sito ad alte prestazioni che ammiri organizza la sua cascata in modo da poter rispecchiare lo schema nel tuo design system.
Accorcia anche la parte noiosa del punto tre qui sopra. Quando converti un componente runtime in classi statiche, avere gli stili calcolati finali in una forma modificabile, piuttosto che come stili inline sparsi e blocchi <style> iniettati, rimuove la maggior parte del lavoro di trascrizione.
Punti chiave
- La generazione di stili runtime è un lavoro JavaScript sul thread principale che avviene dopo il download di JavaScript, direttamente sul tuo percorso critico di rendering.
- Il CSS statico viene analizzato una volta, memorizzato nella cache per un anno quando è sottoposto a content hashing e condiviso tra ogni route e ogni navigazione.
- Fai l'inline solo delle regole di cui il primo viewport ha bisogno; differisci il resto dietro un
<link>non bloccante e una politica di cache lunga. - Le proprietà personalizzate CSS più
@layercoprono la stragrande maggioranza dei casi che richiedevano l'interpolazione di stili guidata da proprietà. - Migra in modo incrementale, componente per componente, e misura il paint timing p75 e i long task piuttosto che le medie.
Distribuire più CSS sembra una regressione finché non realizzi che stai scambiando un foglio di stile memorizzato nella cache e analizzato una volta con megabyte di JavaScript che lo ricostruivano a ogni caricamento di pagina.
La lezione si generalizza ben oltre GitHub. Ogni astrazione che viene eseguita nel browser è un costo pagato dall'utente, sul suo dispositivo, sulla sua rete, ogni singola volta. Spostare la generazione degli stili al build time è uno degli scambi a più alta leva disponibili per un frontend guidato dai componenti: distribuisci un file statico che il browser sa già come gestire ed elimini il meccanismo che gli si metteva in mezzo.
