React

loading.tsx e skeleton senza salti di layout

loading.tsx crea un boundary di Suspense: come scrivere uno skeleton con la stessa forma del contenuto, per non aggiungere CLS.

In sintesi

  • `loading.tsx` è un boundary di Suspense creato dalla convenzione di file.
  • Lo skeleton deve avere stessa altezza e stesse colonne del contenuto finale.
  • Sotto i 200 ms di attesa lo skeleton è peggio del vuoto.

loading.tsx avvolge un segmento in un boundary di Suspense e mostra quel file mentre il server prepara la pagina. Uno skeleton che cambia altezza rispetto al contenuto reale produce CLS, non l’effetto di velocità.

Cosa fa davvero loading.tsx

Il file è zucchero sintattico: Next crea un <Suspense fallback={<Loading />}> attorno al segmento. Non serve import, non serve configurazione, e vale per la pagina e per i segmenti figli che non hanno un proprio loading.tsx.

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    
      {Array.from({ length: 6 }).map((_, i) => (
        
      ))}
    
  );
}

La regola che evita il salto

Lo skeleton deve occupare lo spazio finale, non uno spazio plausibile. Se la card vera è alta 128 px, lo skeleton è 128 px. Se la griglia ha tre colonne a 768 px, lo skeleton ha le stesse tre colonne allo stesso breakpoint.

ElementoCosa copiareCosa evitare
AltezzaIl valore finale in px o in righe di testoUn h-24 provvisorio
GrigliaStesse colonne e stesso gapUn flex diverso dal risultato
TestoNumero di righe plausibileParole finte di lunghezza diversa
ImmagineLo aspect-ratio del contenutoUn quadrato generico

Quando il salto arriva lo stesso

Due cause ricorrenti: un font che cambia le metriche quando arriva, e un’immagine senza dimensioni. Il primo si risolve con next/font e una metrica di fallback; il secondo con width e height o aspect-ratio. I dettagli sono in layout shift da immagini senza dimensioni e in CLS: cause e correzioni.

Il budget è numerico: CLS sotto 0,1, misurato il 1 novembre 2026 su una pagina del catalogo EneUI che indicizza 111.985 componenti da 1.472 librerie. Lo skeleton è parte del budget, non un dettaglio estetico.

Tre record: i componenti skeleton con le forme base, gli stati di caricamento per spinner e progress, e i blocchi dashboard per una griglia che regge il caricamento.

Se il boundary deve gestire un errore e non solo l’attesa, il pattern è in error boundary nell’App Router. Lo streaming che rende utile il fallback è in streaming e Suspense. La struttura delle cartelle è in struttura App Router.

Il limite da mettere in conto

Un loading.tsx al livello del layout radice mostra lo skeleton anche per navigazioni che durano 80 ms: sotto i 200 ms l’attesa non si vede e il lampo è peggio del vuoto.

Domande frequenti

loading.tsx è obbligatorio?

No. Serve quando il segmento ha un’attesa reale. Un progetto senza <code>loading.tsx</code> funziona: l’utente vede la pagina precedente finché la nuova non è pronta.

Come evito il lampo dello skeleton?

Alzando il ritardo di comparsa con una transizione o mettendo il file solo sui segmenti lenti. Sotto i 200 ms l’attesa non si percepisce e il fallback peggiora la sensazione.

Posso animare lo skeleton?

Sì, con <code>animate-pulse</code> o un gradiente. Evita animazioni che spostano il layout: il pulse cambia solo l’opacità, non la dimensione.

Dove metto il loading per una singola sezione?

Con un <code>&lt;Suspense&gt;</code> esplicito attorno al componente lento. <code>loading.tsx</code> copre l’intero segmento, non una parte.

Perché la pagina salta quando arriva il contenuto?

Perché lo skeleton ha un’altezza diversa, o perché il font e le immagini cambiano le metriche. Confronta il rettangolo finale con quello del fallback, non a occhio.