O Suspense mostra um fallback, como uma mensagem de carregamento, enquanto os componentes dentro dele ainda não estão prontos. O React.lazy é a coisa mais comum pela qual esperar: ele carrega o código de um componente só quando esse componente renderiza pela primeira vez, o que mantém o download inicial pequeno.
Clique em Show chart: Loading chart... aparece por um segundo e depois o gráfico. Esconda e mostre de novo, e ele aparece na hora sem nova linha de log, porque o lazy guarda o módulo carregado.
Code splitting com lazy
O editor guarda tudo em um arquivo só, então o exemplo monta o módulo lento à mão: uma promise que resolve para um objeto com um export default depois de um segundo. Em um app real, o componente fica no próprio arquivo e você passa um import dinâmico:
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart.jsx'));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
);
}
import('./Chart.jsx') retorna uma promise para o módulo. Bundlers como o Vite e o webpack veem o import dinâmico e colocam Chart.jsx e tudo o que só ele usa em um arquivo separado, baixado na primeira vez que <Chart /> renderiza. Regras para saber:
- O módulo precisa de um export default. O
lazylê a propriedadedefaultdaquilo para o que a promise resolve. Para um export nomeado, faça o mapeamento:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))). - Chame o
lazyno nível superior de um módulo. Dentro de um componente, ele criaria um tipo de componente novo a cada renderização, então o React desmontaria o antigo, perderia o estado dele e o carregaria de novo. - Divida onde o usuário já espera de qualquer jeito. Rotas, modais, painéis abertos raramente e widgets pesados (editores, gráficos, mapas) são bons candidatos. Dividir cada componente pequeno acrescenta requisições e estados de carregamento sem ganho nenhum.
Pré-carregando antes do clique
Um componente lazy começa a carregar quando renderiza pela primeira vez, então o usuário sempre espera pelo menos um download depois de clicar. Se dá para prever que o clique vem aí, comece o download antes. Guarde a função de import em uma variável e chame-a no hover ou no foco; o navegador guarda o módulo, então quando o lazy chamar o mesmo import depois, ele resolve sem um segundo download.
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Como o Suspense decide o que mostrar
Quando um componente dentro de <Suspense> não está pronto, ele suspende: o React para de renderizar essa parte e mostra o fallback do Suspense mais próximo acima dele. Tudo dentro desse boundary é substituído pelo fallback, não só o componente que está esperando. Quando aquilo pelo que ele esperava fica pronto, o React renderiza o conteúdo de novo e substitui o fallback.
Isso faz da posição do boundary uma decisão de design. Coloque partes independentes em boundaries próprios para que cada uma apareça quando estiver pronta:
Primeiro a página inteira mostra Loading page..., porque Header pertence ao boundary de fora. Quando o cabeçalho fica pronto, o artigo aparece com Loading comments... abaixo dele, e os comentários chegam por último. Troque 1500 por 3000 e a prévia recarrega com uma espera maior só para os comentários. Apague o <Suspense> de dentro (mantenha <Comments />) e a página espera os comentários antes de mostrar qualquer coisa.
Suspense com use() no React 19
No React 19 um componente pode ler uma promise com use(promise). Se a promise ainda está pendente, o componente suspende e o Suspense mais próximo mostra o fallback; quando ela resolve, o use retorna o valor. A página do hook use trata disso por completo. O fetchUser falso abaixo faz o papel de uma requisição real.
Passe pelos usuários e depois volte para User 1: ele aparece na hora e o Console não registra um novo fetch, porque a promise já está no cache.
Por que a promise precisa ficar em cache
O mapa cache não é uma otimização aqui, ele é obrigatório. Um componente que suspende não guarda nada daquela tentativa: quando a promise resolve, o React o renderiza de novo desde o início. Se Profile chamasse fetchUser(id) diretamente, cada tentativa criaria uma promise nova, iniciaria uma requisição nova e suspenderia nela de novo. O perfil nunca aparece, e o Console se enche de linhas fetching user. Então a promise precisa vir de algum lugar que sobreviva à renderização:
- um cache indexado pela requisição, como o mapa acima (bibliotecas de dados como TanStack Query e os loaders dos frameworks fazem isso por você);
- um pai que cria a promise uma vez, em um event handler ou em um Server Component, e a passa para baixo como prop.
Repare que cada novo usuário ainda substitui o perfil pelo fallback. Se você prefere manter o usuário antigo na tela até o próximo ficar pronto, envolva a atualização em uma transição: startTransition(() => setId(n)). O React não esconde conteúdo que já está visível em uma transição (veja useTransition).
Erros precisam de um error boundary
O Suspense trata da espera, não da falha. Se um import lazy falha (o usuário ficou offline, um deploy novo removeu o chunk antigo) ou uma promise passada para o use rejeita, o React lança o erro para o error boundary mais próximo. Sem um, a árvore inteira abaixo da raiz é desmontada. Error boundaries ainda são componentes de classe (veja error boundaries):
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
O que o Suspense não detecta
O Suspense só reage a componentes que suspendem: componentes lazy, use(promise) e fontes de dados feitas para o Suspense (loaders de frameworks, bibliotecas com suporte a Suspense). Um fetch dentro de useEffect que define estado quando termina não suspende, então um boundary de Suspense em volta dele nunca mostra o fallback. Para esse padrão, você mantém o próprio estado de loading, como mostra a página sobre buscar dados.
Perguntas frequentes
O que é o Suspense do React?
<Suspense fallback={...}> é um componente que mostra o fallback enquanto algum componente dentro dele está esperando por algo, como código carregado sob demanda ou dados lidos com use. Quando tudo dentro dele fica pronto, o React troca o fallback pelo conteúdo.
O que o React.lazy faz?
lazy(() => import('./Chart.jsx')) cria um componente cujo código é baixado na primeira vez que ele renderiza. Os bundlers colocam esse arquivo em um chunk separado, então a página inicial carrega menos JavaScript.
O Suspense funciona para buscar dados?
Sim, quando a fonte de dados o suporta. No React 19 um componente pode ler uma promise com use(promise) e suspender até ela resolver. Frameworks como o Next.js também integram o Suspense ao carregamento de dados. Um fetch dentro de useEffect não dispara o Suspense.
Como trato erros com o Suspense?
O Suspense só trata da espera. Se um import lazy ou uma promise falha, o erro vai para o error boundary mais próximo, então envolva o boundary de Suspense (ou o pai dele) em um.
Onde devo chamar o lazy?
No nível superior de um módulo, fora de qualquer componente. Chamar lazy dentro de um componente cria um tipo de componente novo a cada renderização, o que reinicia o estado dele e o carrega de novo.