// page.tsx
import { Suspense } from 'react';
import { StatsCard, PaymentsTable, SlowActivityFeed } from '@/components';
import { SkeletonFeed } from '@/components/skeletons';
export default async function DashboardPage() {
// Datele rapide se pot prelua direct la nivel de pagină
return (
<div className="grid gap-6">
<StatsCard />
<Suspense fallback={<SkeletonFeed />}>
<SlowActivityFeed />
</Suspense>
<PaymentsTable />
</div>
);
}Ne-am aruncat toți pe Next.js App Router și am zis că loading.tsx e sfânt pentru că "vine direct din cutie". Doar că, la primul proiect serios cu dashboard-uri complexe și vreo 12 API-uri externe, am realizat că o pagină albă cu un spinner gigant e un UX mizerabil. Azi vreau să lămurim când merită să spargi layout-ul cu <Suspense> granular și cum afectează asta streaming-ul SSR.
Capcana din loading.tsx
Am avut un caz la un proiect cu vreo 4.000 de utilizatori activi pe zi. Aveam un dashboard cu trei secțiuni principale: statistici rapide, un feed de activitate (care făcea un query SQL greu de vreo 1.8 secunde) și istoricul de plăți. Inițial, am trântit loading.tsx la nivel de rută.
Rezultatul? Toată pagina stătea blocată într-un skeleton loading general timp de aproape două secunde, deși statisticile rapide și plățile erau gata în 150 de milisecunde.
Aici e marea confuzie. loading.tsx este, sub capotă, doar un <Suspense> global care îmbracă întreaga pagină (mai exact, layout.tsx e randat, iar page.tsx e pus în Suspense). E excelent pentru tranziții rapide între pagini complet diferite, dar e groaznic când ai date eterogene pe aceeași pagină.
Când coborâm în tranșee cu Suspense granular
Trade-off-ul e simplu: loading.tsx înseamnă zero efort de scriere, dar blochează tot. <Suspense> granular cere să muți fetch-ul de date în componente de tip Server Components dedicate și să le înfășori manual.
Uite cum am optimizat dashboard-ul respectiv. Am lăsat shell-ul paginii să se randeze instant (SSR-ul livrează header-ul, sidebar-ul și scheletul de bază), iar feed-ul de activitate l-am izolat complet într-o componentă separată.
Prin abordarea asta, utilizatorul vede layout-ul și datele rapide aproape instantaneu. Doar zona feed-ului afișează o stare de încărcare localizată, lăsând restul paginii complet interactiv.
Cum funcționează de fapt Streaming-ul SSR?
Mulți cred că SSR înseamnă că serverul generează tot HTML-ul și-l trimite la client. Așa era pe vremuri. Cu streaming (activat automat când folosești Suspense în React 18 / Next.js), serverul trimite imediat bucata de HTML care e gata (de exemplu, meniul și layout-ul).
Conexiunea HTTP rămâne deschisă. Când componenta ta lentă își termină fetch-ul pe server, React randează HTML-ul pentru ea și-l "împinge" pe aceeași conexiune, împreună cu un script inline minuscul care înlocuiește fallback-ul (skeletonul) cu conținutul real în DOM. Browserul nu trebuie să facă un nou request API de la client, totul se întâmplă nativ pe prima conexiune. Am măsurat o scădere de 30% la Largest Contentful Paint (LCP) doar făcând mutarea asta.
Trade-off-ul de care nimeni nu vorbește
Sună ideal, dar ai grijă la layout shifts. Dacă nu pui un fallback cu înălțime fixă (un skeleton care mimează exact dimensiunile componentei finale), pagina o să "sară" când vin datele. Și crede-mă, nimic nu enervează un user mai mult decât să dea click pe un buton și ecranul să se mute cu 200px mai jos exact în acea milisecundă.
Regula mea de aur: folosește loading.tsx doar ca un safety net general pentru rute noi sau pagini simple de conținut. Pentru orice dashboard sau pagină unde ai mai mult de două surse de date, sparge totul în Server Components și controlează loading-ul local.
Voi cum gestionați asta? Preferați să aduceți totul într-un singur query masiv pe pagină sau spargeți în bucăți mici cu Suspense?