Streaming e Suspense no React

1. Introdução ao Streaming e Suspense no React

O React moderno introduziu duas poderosas abstrações que transformaram a forma como construímos aplicações web: Streaming e Suspense. Streaming permite que o servidor envie partes do HTML progressivamente ao cliente, enquanto o Suspense oferece um mecanismo declarativo para lidar com estados de carregamento em componentes assíncronos.

No modelo tradicional, o servidor precisa processar toda a árvore de componentes antes de enviar qualquer HTML ao navegador. Com Streaming + Suspense, o React pode começar a enviar conteúdo imediatamente, reduzindo drasticamente o TTFB (Time to First Byte) e permitindo que o usuário interaja com partes da página enquanto outras ainda estão sendo processadas.

2. Suspense para Carregamento de Dados no Cliente

O uso mais básico do Suspense no cliente é com React.lazy para code splitting:

import React, { Suspense } from 'react';

const DashboardComponent = React.lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Carregando dashboard...</div>}>
      <DashboardComponent />
    </Suspense>
  );
}

Para integração com bibliotecas de data fetching como React Query:

import { useSuspenseQuery } from '@tanstack/react-query';
import { Suspense } from 'react';

function UserProfile() {
  const { data } = useSuspenseQuery({
    queryKey: ['user', 1],
    queryFn: () => fetch('/api/user/1').then(res => res.json())
  });

  return <div>{data.name}</div>;
}

function Page() {
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile />
    </Suspense>
  );
}

Para transições suaves, use startTransition:

import { startTransition } from 'react';

function handleSearch(query) {
  startTransition(() => {
    setSearchQuery(query);
  });
}

3. Streaming no Servidor com React 18 e Node.js

No servidor Node.js, o renderToPipeableStream permite enviar HTML progressivamente:

import express from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

const app = express();

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    onShellReady() {
      res.setHeader('Content-Type', 'text/html');
      res.write('<html><head><title>Streaming App</title></head><body><div id="root">');
      pipe(res);
      res.write('</div></body></html>');
    },
    onError(err) {
      console.error(err);
      res.statusCode = 500;
      res.end('Erro interno');
    }
  });
});

Diferentemente do renderToString (que bloqueia até processar tudo), o renderToPipeableStream começa a enviar o "shell" da página imediatamente, enquanto componentes envolvidos em Suspense são enviados conforme ficam prontos.

4. Suspense no Servidor: Componentes Assíncronos e Data Fetching

No servidor, o Suspense permite pausar a renderização de partes específicas da árvore enquanto dados são buscados:

import { Suspense } from 'react';

async function fetchComments(postId) {
  const res = await fetch(`https://api.example.com/posts/${postId}/comments`);
  return res.json();
}

function Comments({ postId }) {
  const comments = use(fetchComments(postId));

  return (
    <ul>
      {comments.map(comment => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}

function BlogPost({ postId }) {
  return (
    <article>
      <h1>Post Title</h1>
      <p>Conteúdo principal...</p>
      <Suspense fallback={<div>Carregando comentários...</div>}>
        <Comments postId={postId} />
      </Suspense>
    </article>
  );
}

Estratégias de fallback no servidor podem incluir conteúdo estático ou placeholders customizados que mantêm o layout estável.

5. Construindo uma Aplicação com Streaming e Suspense (Exemplo Prático)

Aqui está um exemplo completo com Express.js:

import express from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import React, { Suspense } from 'react';

// Componente que simula busca lenta de dados
function SlowComponent({ delay }) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(<div>Dados carregados após {delay}ms</div>);
    }, delay);
  });
}

function HomePage() {
  return (
    <html>
      <head><title>Streaming App</title></head>
      <body>
        <h1>Minha Página Streamada</h1>
        <Suspense fallback={<div>Carregando parte 1...</div>}>
          <SlowComponent delay={2000} />
        </Suspense>
        <Suspense fallback={<div>Carregando parte 2...</div>}>
          <SlowComponent delay={4000} />
        </Suspense>
        <footer>Footer - carregado imediatamente</footer>
      </body>
    </html>
  );
}

const server = express();

server.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<HomePage />, {
    onShellReady() {
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },
    onError(err) {
      console.error(err);
      res.statusCode = 500;
      res.end('Erro');
    }
  });
});

server.listen(3000, () => {
  console.log('Servidor rodando em http://localhost:3000');
});

O navegador receberá primeiro o shell da página (título, footer), depois cada seção Suspense conforme fica pronta.

6. Boas Práticas e Considerações de Performance

Quando usar Streaming:
- Páginas com conteúdo dinâmico de diferentes fontes
- Aplicações que precisam de First Paint rápido
- Conteúdo acima da dobra que pode ser servido imediatamente

Cuidados importantes:
- Metadados e SEO: use tags no shell inicial
- Cache: configure caching adequado para partes estáticas
- Debugging: erros em stream são mais complexos de rastrear

// Estratégia de cache com streaming
app.get('/', (req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=300');
  // Streaming continua funcionando com cache
});

7. Comparação com Abordagens Tradicionais e Futuro

Métricas comparativas:

Métrica renderToString renderToPipeableStream
TTFB Alto (processa tudo) Baixo (envia shell)
First Paint Após processamento total Imediato
Interação Aguarda hidratação total Progressiva

O Streaming + Suspense é a base para os Server Components no React 18+, que levam essa abordagem ao próximo nível. Frameworks como Next.js (com App Router) e Remix já estão adotando essas APIs nativamente.

O futuro aponta para uma web onde o servidor envia componentes interativos sob demanda, combinando o melhor da renderização server-side com a riqueza de interações client-side.

Referências