Capítulo 83 de 456
Streaming envia partes da resposta HTML assim que ficam prontas (chunked transfer encoding), em vez de esperar a página inteira renderizar — o navegador começa a pintar enquanto o servidor ainda gera o resto, alinhado a boundaries <Suspense>.
<Suspense> resolve, React envia o HTML pronto + <script> que faz o swap no DOM instantaneamente, sem esperar o bundle JS carregar.rsc: 1), sem HTML.loading.js: envolve automaticamente page.js num <Suspense> boundary usando o arquivo como fallback; escopo é a página inteira; é prefetched como fallback instantâneo na navegação.<Suspense> granular: permite controlar exatamente quais seções streamam independentemente; boundaries irmãs resolvem em paralelo sem se bloquear; boundaries aninhadas criam revelação progressiva (nested).params, searchParams, cookies(), headers() e fetches até o componente que realmente precisa — await no topo do layout/page torna tudo abaixo dinâmico e não-prerenderizável.use(): hook React para ler uma Promise passada como prop; só o componente que chama use() precisa de <Suspense> ao redor.notFound() mid-stream vira <meta name="robots" content="noindex"> em vez de 404 real; redirect() mid-stream vira redirect client-side em vez de header HTTP.htmlLimitedBots: bots limitados a HTML (sem execução de JS) recebem metadata bloqueante — Next.js espera generateMetadata resolver antes de streamar; browsers completos recebem streaming metadata.import { Suspense } from 'react'
import { Revenue } from './revenue'
import { RecentOrders } from './recent-orders'
export default function Dashboard() {
return (
<div>
<Suspense fallback={<p>Loading revenue...</p>}>
<Revenue />
</Suspense>
<Suspense fallback={<p>Loading orders...</p>}>
<RecentOrders />
</Suspense>
</div>
)
}
const cookieStore = cookies() // Start the work, but don't await
return (
<div>
<Nav>
<Suspense fallback={<p>Loading user...</p>}>
<UserMenu cookiePromise={cookieStore} />
</Suspense>
</Nav>
{children}
</div>
)
<Nav> e {children} entram no static shell porque nada no layout dá await; só <UserMenu> suspende ao resolver a Promise de cookies.const { slug } = await params
const exists = await checkSlugExists(slug) // Fast existence check
if (!exists) notFound() // Real 404, before any Suspense boundary
return (
<Suspense fallback={<p>Loading post...</p>}>
<PostContent slug={slug} />
</Suspense>
)
notFound() deve rodar antes de qualquer await/boundary que já tenha iniciado o streaming.module.exports = {
async headers() {
return [{ source: '/:path*{/}?', headers: [{ key: 'X-Accel-Buffering', value: 'no' }] }]
},
}
loading.js | <Suspense> | |
|---|---|---|
| Escopo | Página inteira | Qualquer componente |
| Setup | Colocar um arquivo | Envolver componentes explicitamente |
| Navegação | Prefetched como fallback instantâneo | Não prefetched por padrão |
| Melhor para | Páginas onde nada renderiza sem dado | Maioria das páginas, controle granular |
| Plataforma | Suporte a streaming |
|---|---|
| Node.js server | Sim |
| Docker container | Sim |
| Static export | Não |
| Adapters | Depende da plataforma |
await cookies()/headers()/params no topo do layout ou page: bloqueia tudo abaixo daquele ponto de ser prerenderizado como parte do static shell.<Suspense>: ele só pinta quando a boundary resolve e o script de swap executa; mantenha elementos LCP fora/acima de boundaries.curl puro para verificar streaming: curl tem buffering próprio e depende de newlines para exibir linha a linha; use um script que leia response.body como stream, ou o Network tab do DevTools.<Suspense> mais próxima como fallback.use cache, cresce o static shell) e onde colocar as boundaries de <Suspense> (empurradas para perto do acesso dinâmico).use() + Promise-as-prop usado aqui para streaming de dado ao cliente.