Learning
Next.js

Streaming, vízesékek és Suspense

Párhuzamos adatlekérés, React.cache, Suspense határok és kliens navigációs buktatók.

7. Streaming, vízesékek és Suspense

Ha az adatlekéréseket egymás után várod meg, az idő összeadódik (vízesés). Cél: a független munkák párhuzamosan fussanak, és a lassabb részek ne blokkolják az egész oldal első megjelenését.

Vízesés vs párhuzamosság

// ❌ Vízesés – teljes idő ≈ A + B + C
const a = await fetchA()
const b = await fetchB()
const c = await fetchC()

// ✅ Párhuzamos – teljes idő ≈ max(A, B, C)
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()])

Ha B csak A eredményétől függ, először await fetchA(), utána indítsd B-t – ne várj feleslegesen olyan dolgokra, amik még nincsenek a kritikus úton.

Korai promise, késői await

Néha érdemes a promise-okat minél hamarabb elindítani, és csak ott await-elni, ahol tényleg kell az érték. Párhuzamos, de külön Suspense szegmensek:

async function HeaderWithUser() {
  const user = await getUser()
  return <header>{user.name}</header>
}

async function StatsBlock() {
  const stats = await getStats()
  return <section>{/* … */}</section>
}

export default function Page() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <HeaderWithUser />
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsBlock />
      </Suspense>
    </div>
  )
}

A lényeg: ne sorolj felesleges await-et egyetlen async komponens tetején, ha a részek külön is megjelenhetnek – bontsd őket, és használj Suspense-t.

Suspense és streaming

A loading.tsx a mappában lévő page.tsx köré automatikusan Suspense-szerű viselkedést ad. Manuális Suspense határokkal a lassú részek külön „foltokban” tölthetők – a felhasználó hamarabb lát valamit a képernyőn.

import { Suspense } from 'react'

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <Suspense fallback={<p>Oldalsáv betöltése…</p>}>
        <SlowSidebar />
      </Suspense>
      <main>{children}</main>
    </div>
  )
}

React.cache() – deduplikáció fetch nélkül

A fetch egy requesten belüli deduplikációját nem kapod meg automatikusan, ha ORM-mel vagy saját függvénnyel hívsz adatbázist. Ilyenkor a React cache() függvényével ugyanazt a lekérést egy render alatt egyszer futtathatod:

import { cache } from 'react'

export const getUserById = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } })
})

// Layout és Page ugyanazzal az id-vel: egy DB hívás

Ez nem helyettesíti a HTTP cache-t vagy a Next cache tag-eket – csak egy kérésen belüli memoizáció.

useSearchParams, usePathname és Suspense

A useSearchParams() (és bizonyos esetekben más navigációs hookok) miatt a gyerek fa kliens oldali renderelést igényelhet. Ha ezt egy Server Component layout/page közvetlenül tartalmazza, build figyelmeztetés vagy felesleges egész oldalas „kliens bailout” jöhet létre.

Gyakorlati szabály: csomagold a hookot használó részt külön Client Componentbe, és azt tedd Suspense fallbackkel:

// layout.tsx – Server Component
import { Suspense } from 'react'
import { SearchParamsConsumer } from './SearchParamsConsumer'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Suspense fallback={null}>
        <SearchParamsConsumer />
      </Suspense>
      {children}
    </>
  )
}

📝 Összefoglaló – 7. fejezet

  • Független await helyett Promise.all (vagy célzott párhuzamosság)
  • Suspense + loading.tsx: részleges tartalom hamarabb, lassú részek külön
  • cache(): többszöri azonos adatlekérés egy renderben → egy futás
  • Navigációs hookok: Suspense a várt minta a layout/page körül

On this page