Menu

Suspense e lazy no React: carregamento e code splitting

O Suspense mostra um fallback enquanto os componentes dentro dele não estão prontos, e o React.lazy carrega o código de um componente só quando ele renderiza pela primeira vez. Aprenda code splitting, boundaries aninhados, Suspense com use() no React 19 e o tratamento de erros.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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 lazy lê a propriedade default daquilo 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 lazy no 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.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR