Il Flash of Unstyled Content, FOUC, è uno di quei problemi che si presenta sempre nel momento peggiore: durante una demo, in produzione su una connessione lenta, o peggio ancora nei test di performance davanti al cliente. L'utente vede per una frazione di secondo la pagina spoglia, senza stili, poi tutto si sistema. Un attimo breve ma sufficiente a trasmettere l'impressione di qualcosa di rotto. Il FOUC è una conseguenza diretta del modo in cui il browser costruisce la pagina, e capirlo è il primo passo per eliminarlo.
FOUC, perché accade? Rendering pipeline del browser
Quando il browser analizza l'HTML e incontra un tag, blocca il rendering fino al completamento del download e del parsing del foglio di stile. Questo comportamento è intenzionale: il browser evita di dipingere contenuto che verrebbe immediatamente ridisegnato con gli stili applicati. Il FOUC si manifesta quando questo meccanismo di blocco viene aggirato o cortocircuitato.
Le cause più frequenti sono tre. La prima è un foglio di stile: il browser ha già renderizzato l'HTML e non blocca più il caricamento. La seconda è JavaScript che inietta dinamicamente un tag o un blocco dopo che la pagina è già visibile. La terza, più sottile, riguarda i CSS-in-JS e i framework component-based che costruiscono gli stili a runtime: se JavaScript arriva in ritardo o viene eseguito dopo il primo paint, l'utente vede il DOM nudo per il tempo che intercorre tra il paint e l'iniezione degli stili.
La soluzione classica: visibilità nascosta in attesa degli stili
Il pattern più longevo per prevenire il FOUC è nascondere il contenuto fino a quando gli stili non sono pronti, poi rivelarlo. Nella sua forma più semplice si imposta visibility: hidden sull'elemento radice e si rimuove quella regola non appena gli stili principali sono stati caricati. La distinzione tra visibility: hidden e display: none è importante: il primo preserva il layout e non causa reflow quando viene rimosso, il secondo sì.
<!-- Prima di qualsiasi foglio di stile esterno -->
.no-fouc { visibility: hidden; }
...
// Rimuove la classe non appena il DOM e gli stili sono pronti
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.remove('no-fouc');
});
Questa tecnica funziona, ma ha un difetto: se JavaScript non viene eseguito per qualsiasi motivo, la pagina rimane invisibile. Per questo motivo molti la combinano con un approccio progressivo: si aggiunge la classe no-fouc via JavaScript invece che nell'HTML, così se JS non gira, il contenuto è sempre visibile senza stili piuttosto che invisibile.
// Aggiunge la classe PRIMA del caricamento degli stili
document.documentElement.classList.add('js-loading');
window.addEventListener('load', () => {
document.documentElement.classList.remove('js-loading');
});
/* Nel CSS */
.js-loading * { visibility: hidden; }
Iniezione dinamica degli stili: i casi d'uso legittimi
Prima di entrare nei meccanismi, vale la pena chiarire quando l'iniezione dinamica ha senso. Il caso più comune è il lazy loading degli stili: componenti o sezioni della UI che non vengono mostrati subito non hanno bisogno che i loro stili vengano scaricati al primo caricamento. Caricarli on-demand riduce il peso iniziale della pagina e migliora il Time to Interactive. Un secondo caso è la generazione di stili basata su dati: un tema personalizzato per utente, variabili CSS calcolate da preferenze salvate, o stili che dipendono da valori recuperati via API non possono essere scritti staticamente in un foglio di stile.
Iniettare un tag style con contenuto dinamico
Il modo più diretto è creare un nuovo elemento. Il contenuto può essere qualsiasi stringa CSS valida, incluse le custom properties, le media queries e le regole @keyframes. Per aggiornamenti successivi è consigliabile tenere un riferimento all'elemento e modificarne il contenuto invece di crearne uno nuovo ogni volta, per evitare di accumulare tag inutili nel DOM.
function injectaStili(css, id = 'stili-dinamici') {
let tag = document.getElementById(id);
if (!tag) {
tag = document.createElement('style');
tag.id = id;
document.head.appendChild(tag);
}
tag.textContent = css;
}
// Esempio: applica un tema utente recuperato da API
const tema = await fetch('/api/utente/tema').then(r => r.json());
injectaStili(`
:root {
--colore-primario: ${tema.primario};
--colore-sfondo: ${tema.sfondo};
--font-size-base: ${tema.fontSize}px;
}
`);
Usare textContent invece di innerHTML è una buona abitudine anche quando si lavora con CSS: evita qualsiasi rischio di interpretazione di markup indesiderato, ed è marginalmente più veloce perché il parser non deve analizzare HTML.
Iniettare un foglio di stile esterno e aspettare che carichi
Quando si tratta di un file CSS esterno da caricare on-demand, il tag iniettato dinamicamente non blocca il rendering. Questo è esattamente il vantaggio del lazy loading, ma significa anche che il contenuto potrebbe essere visibile senza stili per il tempo del download. La soluzione è attendere l'evento load del tag prima di mostrare il contenuto che ne dipende.
return new Promise((resolve, reject) => {
// Evita duplicati
if (document.querySelector(`link[href="${href}"]`)) {
resolve();
return;
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = resolve;
link.onerror = () => reject(new Error(`Impossibile caricare ${href}`));
document.head.appendChild(link);
});
}
// Utilizzo: carica gli stili del componente prima di mostrarlo
async function apriModalePesante() {
await caricaFoglioDiStile('/stili/modale-avanzata.css');
document.getElementById('modale').classList.add('visibile');
}
Questo pattern garantisce che il componente diventi visibile solo dopo che i suoi stili sono stati applicati, eliminando il FOUC localizzato senza bloccare il caricamento iniziale della pagina.
CSSStyleSheet costruttibile: il futuro dell'iniezione dinamica
La Web API più moderna per questo problema è la Constructable Stylesheets, disponibile su tutti i browser moderni. Permette di creare un oggetto CSSStyleSheet direttamente in JavaScript, popolarlo con regole CSS, e applicarlo a uno o più documenti o shadow root. Il vantaggio è che un singolo foglio di stile costruttibile può essere condiviso tra più shadow DOM senza duplicazione, e le modifiche vengono propagate automaticamente a tutti i contesti che lo adottano.
// Crea il foglio di stile e inserisce le regole
const foglio = new CSSStyleSheet();
await foglio.replace(`
.card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
padding: 1.5rem;
}
`);
// Applica al documento principale
document.adoptedStyleSheets = [...document.adoptedStyleSheets, foglio];
// Oppure a uno shadow root
const shadow = elemento.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [foglio];
Il metodo replace è asincrono e restituisce una Promise, il che significa che si può attendere il completamento prima di procedere con il rendering del componente. Il metodo replaceSync è la variante sincrona, ma non supporta le regole @import. Per aggiornamenti successivi si può chiamare replace di nuovo sullo stesso oggetto: tutti i documenti e shadow root che lo hanno adottato vedranno immediatamente i nuovi stili.
Evitare il FOUC nei Web Components
I Web Components con shadow DOM hanno un problema FOUC tutto loro: prima che JavaScript esegua e costruisca il componente, il browser mostra l'elemento non definito, spesso con stili di default del browser che non c'entrano nulla con il design voluto. La soluzione è usare il selettore CSS :not(:defined) per nascondere i componenti non ancora registrati, e :defined per rivelarli una volta che JavaScript li ha inizializzato.
/* Nasconde tutti i custom elements non ancora definiti */
:not(:defined) {
visibility: hidden;
}
/* Alternativa: solo per componenti specifici */
mio-componente:not(:defined) {
visibility: hidden;
min-height: 200px; /* preserva lo spazio nel layout */
}
Questo approccio è elegante perché è puramente dichiarativo: non richiede JavaScript aggiuntivo, funziona anche se la registrazione del componente avviene in ritardo, e il browser gestisce la transizione automaticamente nel momento in cui customElements.define() viene chiamato.