// page.tsx - Fără await blocant la nivel de pagină
import { Suspense } from 'react';
import ProductDetails from './ProductDetails';
import ProductReviews, { ReviewsSkeleton } from './ProductReviews';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<main className="p-6">
{/* Renderizat instant la primul chunk SSR */}
<ProductDetails id={params.id} />
{/* Streamed de pe server când se rezolvă API-ul lent */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
</main>
);
}Am văzut mulți colegi care trântesc un loading.tsx în rădăcina unei rute din Next.js și declară că au rezolvat UX-ul. E un început bun, dar când ai un dashboard complex sau o pagină cu date din 3 surse diferite, abordarea asta devine rapid un bottleneck de performanță.
Haideți să vedem ce se întâmplă sub capotă și de ce loading.tsx s-ar putea să vă încetinească aplicația fără să realizați.
Ce face de fapt loading.tsx sub capotă
Fișierul loading.tsx oferit de Next.js App Router este practic un syntactic sugar. Sub capotă, Next.js împachetează automat componenta page.tsx într-un <Suspense fallback={<Loading />}> la nivel de rută.
Problema e că e un sistem "totul sau nimic". Dacă page.tsx este o componentă async care așteaptă după 3 apeluri de API în await, browserul va afișa skeleton-ul din loading.tsx până când cel mai lent apel se termină. Dacă API-ul de recomandări durează 900ms, utilizatorul vede skeleton-ul timp de 900ms, deși detaliile principale ale produsului erau gata în 80ms.
Când treci la Suspense granular și Streaming SSR
Soluția este să scoți await-ul din nivelul rădăcină al paginii și să folosești boundary-uri de <Suspense> direct în arborele de componente. Pagina principală randază scheletul HTML dinamic instant (TTFB sub 100ms), iar serverul face stream la bucățile de HTML pe măsură ce datele devin disponibile pe aceeași conexiune HTTP.
Am testat chestia asta anul trecut pe un proiect de e-commerce cu 40k utilizatori zilnici. Am scos fetch-ul de recenzii și cel de produse similare din page.tsx și le-am mutat în componente Server async separate, împachetate în <Suspense>.
Rezultatul? First Contentful Paint (FCP) a scăzut de la 1.4s la 320ms, iar Largest Contentful Paint (LCP) de la 2.2s la 1.1s. Utilizatorul vedea titlul, prețul și imaginile instant, în timp ce secțiunea de jos se încărca fluid secundar.
Trade-off-uri: Când devine nasol
Totuși, Suspense-ul granular are capcanele lui. Nu e glonțul de argint.
- Efectul de popcorn (Layout Shifts): Dacă pui câte un
<Suspense>pentru fiecare widget mic din sidebar, pagina va „sări” în toate părțile pe măsură ce componentele se încarcă în ordine aleatorie. Soluția e să grupezi widget-urile conexe sub un singur Suspense. - Waterfall-uri neintenționate: Dacă o componentă suspendată randază altă componentă copil care face la rândul ei fetch async, creezi o cascadă de rețea pe server. Asigură-te că declanșezi fetch-urile în paralel unde e posibil.
Regula mea de aur: folosesc loading.tsx doar pentru tranziții majore la navigare între pagini complet diferite, și <Suspense> explicit în interiorul paginii pentru date cu latență impredictibilă sau terțe (analytics, recomandări, stocuri).
Voi ce strategie folosiți la proiectele mari? Bazați doar pe loading.tsx sau aveți boundary-uri manuale?