eduardweb.
Hooks & PatternsAvansat#performance#nextjs#react#ssr

React Suspense vs loading.tsx: Când cobori cu bisturiul în streaming-ul din Next.js

De Sorin Tudor, 18 iun. 2026 · 16 vizualizări · 3 like-uri

Postat 18 iun. 2026
typescript
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { TransactionsList, TransactionsSkeleton } from './components/Transactions';
import { CurrencyWidget, WidgetSkeleton } from './components/Currency';

export default async function DashboardPage() {
  return (
    <main className="p-6 grid gap-6 grid-cols-1 md:grid-cols-3">
      <div className="md:col-span-2">
        {/* Datele rapide se încarcă direct în stream-ul inițial */}
        <Suspense fallback={<TransactionsSkeleton />}>
          <TransactionsList />
        </Suspense>
      </div>
      
      <div className="col-span-1">
        {/* Widget-ul lent este izolat; nu blochează restul paginii */}
        <Suspense fallback={<WidgetSkeleton />}>
          <CurrencyWidget />
        </Suspense>
      </div>
    </main>
  );
}

Ne lovim des de dilema asta în Next.js App Router: lăsăm framework-ul să se ocupe de stările de încărcare prin loading.tsx sau punem manual bariere de Suspense prin componente? Ambele folosesc streaming sub capotă, dar diferența de UX e uriașă.

Am pățit-o chiar anul trecut la un proiect cu vreo 12.000 de utilizatori activi pe zi. Aveam un dashboard financiar încărcat cu grafice, tranzacții recente și un widget de conversie valutară care apela un API extern destul de leneș (răspundea cam în 1.5 - 2 secunde).

Inițial, am mers pe varianta comodă. Am trântit un loading.tsx în folderul /dashboard și am zis că am rezolvat problema.

Ce se întâmplă când folosești loading.tsx ca o „bâtă”

Next.js folosește loading.tsx pentru a îmbrăca automat întreaga pagină (sau sub-rută) într-o barieră de Suspense. Sună bine la nivel teoretic, dar practic am distrus experiența utilizatorului.

De fiecare dată când userul intra pe dashboard, ecranul devenea alb sau arăta un schelet masiv timp de două secunde. Deși lista de tranzacții și profilul utilizatorului erau gata de randat în 200ms, ele erau ținute oaspete de API-ul extern de conversie valutară. Practic, am egalat performanța la cel mai lent numitor comun.

Aici intervine magia streaming-ului prin SSR. Cu HTTP streaming, serverul Node.js nu mai așteaptă ca toate datele să fie gata pentru a trimite HTML-ul în browser. Trimite imediat scheletul static (meniul, header-ul), apoi, pe măsură ce datele vin de la baze de date sau API-uri, trimite restul de HTML prin aceeași conexiune deschisă.

Soluția: React Suspense la nivel de componentă

Am șters loading.tsx de la nivelul rutei și am coborât Suspense direct în pagină, încapsulând doar componentele asincrone care făceau fetch de date grele.

Prin mutarea asta simplă, am redus TTI-ul (Time to Interactive) pe pagină cu peste 60%. Userul vedea meniul, profilul și tranzacțiile instantaneu. În locul widget-ului valutar apărea un loader discret, care se transforma în date reale după cele două secunde de așteptare.

Trade-off-ul sincer

Deși Suspense granular oferă cel mai bun UX, vine la pachet cu un cost de mentenanță.

  • loading.tsx e excelent pentru rute simple, pagini de prezentare sau când vrei să blochezi navigarea până când tot layout-ul de bază e gata. Efort de scriere: zero secunde.
  • Suspense granular înseamnă că trebuie să gestionezi stările de eroare (ErrorBoundary) și scheletele de loading pentru fiecare widget în parte. Dacă ai 10 widget-uri pe pagină, poți ajunge la un layout extrem de fragmentat, unde elementele sar pe ecran (layout shift) în momente diferite.

Regula mea de aur acum? Folosesc loading.tsx doar pentru scheletul global al paginii (sidebar, navbar) și Suspense chirurgical în interiorul paginii pentru orice fetch care durează mai mult de 300ms.

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.