Voltar ao blog

React 18: Suspense e Concurrent Mode para Interfaces Super Rápidas

React 18: Suspense e Concurrent Mode para Interfaces Super Rápidas

Em aplicações React complexas, é comum sofrer com carregamento lento e UI travando quando muitos dados ou componentes precisam ser carregados. O React 18 introduz novidades importantes — como o Suspense e o Modo Concorrente (Concurrent Mode) — que ajudam a otimizar o carregamento assíncrono de componentes e dados. Com essas ferramentas, podemos manter a interface fluida mesmo quando tarefas pesadas estão em andamento. Neste artigo, você verá como o Suspense e os recursos concorrentes do React 18 funcionam, com exemplos práticos de código, para acelerar suas aplicações React.

A partir do React 18, o sistema de renderização foi reestruturado com um novo "renderizador concorrente" que permite operações como interrupção de render, transições de estado, automatic batching, e melhorias no server-side rendering (SSR). Na prática, o React passa a preparar múltiplas versões da interface ao mesmo tempo e a lidar com atualizações urgent e não-urgent separadamente (he.legacy.reactjs.org) (he.legacy.reactjs.org). Graças a isso, mesmo enquanto seu app faz cálculos demorados ou carrega dados, React pode ceder o controle ao navegador e manter a UI responsiva (he.legacy.reactjs.org) (3perf.com).

O que você vai aprender neste artigo: conceitos básicos de Suspense e Modo Concorrente no React 18, como usá-los para melhorar a experiência do usuário, exemplos de código passo-a-passo, e melhores práticas para transições e carregamento assíncrono. Vamos lá!

O que são o Suspense e o Modo Concorrente no React 18?

Antes de começar a usar essas ferramentas, é importante entender o que elas fazem:

  • React Suspense: É um recurso nativo do React que permite a um componente “pausar” sua própria renderização até que algo assíncrono esteja pronto — por exemplo, um import dinâmico ou uma requisição de dados. Enquanto o componente não está pronto, o React exibe um fallback (uma tela de carregamento, spinner, placeholder, etc.) até que a promessa assíncrona seja concluída. Em outras palavras, Suspense permite renderizar de forma declarativa uma UI alternativa enquanto esperamos por componentes ou dados (blog.logrocket.com). Inicialmente criado para code splitting com React.lazy, o Suspense agora se integra profundamente com o novo modelo concorrente do React 18.

  • Modo Concorrente (Concurrent Mode): Embora o nome “modo concorrente” não apareça diretamente como API no React 18 (ele ocorre por padrão ao usar os novos recursos), a ideia é que o React pode intercalar renderizações de forma não bloqueante. Com o modo concorrente, o React pode pausar uma renderização em andamento (por exemplo, uma atualização de grande porte) e retomar depois, dando prioridade a atualizações mais urgentes (como input do usuário) (he.legacy.reactjs.org) (3perf.com). Isso resulta numa interface que responde imediatamente às ações do usuário, sem travar tudo para finalizar outras tarefas em background. Graficamente, imagine que o React está trabalhando na construção de uma tela complexa em segundo plano, mas se você clicar ou digitar algo, ele rapidamente troca o foco para responder à sua ação, e depois volta ao trabalho anterior.

O React 18 traz vários recursos novos de concorrência integrados:

  • Batched Updates (Agrupamento Automático de Atualizações): Antes do React 18, apenas atualizações no mesmo evento eram agrupadas. Agora, todas as atualizações de estado (incluindo dentro de Promises, setTimeout, etc.) são automaticamente agrupadas em uma única renderização (3perf.com). Isso reduz re-renderizações desnecessárias. (Para habilitar isso, é preciso usar createRoot do react-dom/client em vez de ReactDOM.render. Veja em seguida.)

  • Transições de UI (useTransition / startTransition): Permite marcar certas atualizações como de baixa prioridade. Por exemplo, quando você digita em um campo de busca, a atualização do texto no input é urgente, mas a atualização da lista de resultados pode ser marcada como não-urgente. Com o hook useTransition, podemos fazer isso, e o React cuidará para não bloquear a digitação enquanto renderiza a lista filtrada (3perf.com). Atualizações não-urgentes não bloqueiam a página, não importa quanto tempo levem (3perf.com). Isso melhora muito a sensação de fluidez da UI.

  • useDeferredValue: É outro hook para dar prioridade ao que é importante. Ele permite atrasar parcialmente a renderização de valores não-críticos. Por exemplo, ao filtrar uma lista grande, o texto digitado no input pode ser mantido atualizado instantaneamente, enquanto a atualização lenta da lista é feita com um valor “deferred”. Assim, a interface reage imediatamente ao input do usuário e só depois refina a lista.

  • Renderização em lote de componentes: Graças ao Suspense, podemos renderizar componentes menores (lazy-loaded) progressivamente, em vez de ter que esperar tudo carregar para mostrar algo.

  • Server-Side Rendering com Suspense: React 18 introduziu streaming SSR onde o servidor envia partes do HTML assim que elas são prontas. O Suspense permite enviar rapidamente o esqueleto da pagina e “completar” o conteúdo à medida que dados/Componentes assíncronos ficam disponíveis, melhorando a performance percebida.

Em resumo, graças a essas melhorias o React 18 atende aos avanços com novas APIs como startTransition, useTransition, useDeferredValue, além do uso de createRoot() para ativar tudo (veja abaixo). O objetivo é tornar sua aplicação mais rápida e responsiva, com menos layout shifts e menos telas de carregamento desnecessárias — em vez disso, mostramos uma sequência suave de estados intermediários (legacy.reactjs.org) (blog.logrocket.com).

Como usar o Suspense no React 18

Agora vamos a exemplos práticos de Suspense. O caso mais simples é usar junto com code-splitting:

Carregando componentes com React.lazy e <Suspense>

Imagine que você tem um componente pesado ou de carregamento lento, carregado dinamicamente. Com React.lazy, você pode importar componentes sob demanda. O Suspense serve como “buffer” que exibe algo enquanto o componente carrega. Veja:

import React, { Suspense } from 'react'; // Componente carregado de forma assíncrona const Gallery = React.lazy(() => import('./Gallery')); function App() { return ( <div> <h1>Minha Galeria de Fotos</h1> <Suspense fallback={<div>Carregando galeria...</div>}> <Gallery /> </Suspense> </div> ); }

No exemplo acima, Gallery é importado apenas quando necessário. Enquanto ele carrega, o React renderiza <div>Carregando galeria...</div> (o fallback). Quando o componente estiver pronto, o React troca pelo componente final. Esse padrão elimina a necessidade de controlar manualmente estado de carregamento, pois o próprio framework trata o suspense.

Você pode ter múltiplos níveis de Suspense. Por exemplo:

const Profile = React.lazy(() => import('./Profile')); const Avatar = React.lazy(() => import('./Avatar')); function Dashboard() { return ( <div> <h2>Painel</h2> <Suspense fallback={<Spinner message="Carregando perfil..." />}> <Profile /> <Suspense fallback={<div>Carregando avatar...</div>}> <Avatar /> </Suspense> </Suspense> </div> ); }

Aqui, Profile e Avatar carregam em paralelo. Primeiro aparece um spinner para o Profile, e um texto separado enquanto o Avatar carrega. O React espera cada um independente graças aos dois <Suspense>. Isso melhora a experiência do usuário, evitando bloqueios desnecessários de toda a tela.

Definição importante (Suspense): “O React Suspense é um recurso incorporado no React para lidar com operações assíncronas. Ele permite que componentes suspendam temporariamente a renderização enquanto esperam por dados assíncronos, exibindo uma UI de fallback (como um spinner) até que os dados estejam disponíveis” (blog.logrocket.com). Ou seja, com Suspense não precisamos fazer manualmente if(carregado) return <...> else return <spinner>, basta lançar uma promessa dentro do componente, que o React exibirá o fallback apropriado (blog.logrocket.com).

Suspense para carregamento de dados (data fetching)

Além de code splitting, o Suspense foi pensado para facilitar “fetch de dados”. Embora o React 18 ainda não inclua por padrão API de fetch integrada, frameworks e bibliotecas (como Relay, SWR, React Query, Next.js) já tiram proveito disso. A ideia é semelhante: o componente “suspende” até os dados chegarem.

Por exemplo, um padrão comum (fetch-on-render) seria:

function UserProfile({ id }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${id}`) .then(res => res.json()) .then(data => setUser(data)); }, [id]); if (!user) return <p>Carregando usuário...</p>; return <div>Nome: {user.name}</div>; }

Com Suspense, poderíamos reescrever usando algo como React.lazy ou uma “promise throwing” (padrão experimental):

const resource = fetchUserResource(42); // supõe que cria uma promise interna function UserProfile({ id }) { const user = resource.read(); // lança promise se não houver dado return <div>Nome: {user.name}</div>; } function App() { return ( <Suspense fallback={<p>Carregando usuário...</p>}> <UserProfile id={42} /> </Suspense> ); }

Na segunda abordagem, resource.read() lançaria internamente uma Promise bloqueando o componente até os dados chegarem, e o React exibiria o fallback. Quando a promise resolve, o suspense “desanda” e renderiza o conteúdo. Isso elimina cenários de waterfall, já que componentes filhos podem carregar dados em paralelo sem aguardar cada useEffect separado.

De forma geral, com React 18 o Suspense se tornou parte estável da renderização concorrente, habilitando recursos como SSR streaming e hydration seletivo de conteúdo (blog.logrocket.com). Resumindo com as palavras da documentação: “Em React 18, o Suspense tornou-se parte estável da renderização concorrente, permitindo recursos como streaming no servidor e integração com frameworks (Next.js, Remix etc.)” (blog.logrocket.com). Mesmo que você não use um framework específico, o padrão já influencia bibliotecas de dados e roteadores modernos, que permitem pré-busca de dados em handlers ou rotas usando Suspense.

Recursos concorrentes do React 18

O Concurrent Mode do React 18 traz várias APIs novas que ajudam a manter a interface ágil:

  • ReactDOM.createRoot e ativações concorrentes: Para usar o modo concorrente, é preciso inicializar sua aplicação com a nova API createRoot. Em vez de ReactDOM.render(<App/>, root), faça:

    import ReactDOM from 'react-dom/client'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App />);

    Esse simples ajuste ativa o novo reconciliador do React 18. A partir daí, o React passa a aplicar automatic batching e renderização concorrente por padrão. Sem usar createRoot, o aplicativo continua em modo síncrono (como no React 17).

  • Agrupamento automático de atualizações (batching): Após createRoot, todas as atualizações de estado são agrupadas, mesmo dentro de callbacks assíncronos. Isso reduz renderizações intermediárias. Por exemplo, se você fizer setState dentro de um setTimeout, o React 18 agrupa isso em um único re-render, coisa que não acontecia antes. Em analogia, é como anotar todos os itens da sua lista de compras e ir ao mercado fazer um passeio só, em vez de voltar várias vezes separadamente. Essa melhoria fica transparente ao desenvolvedor, mas faz a UI processar menos renderizações internas.

  • startTransition e useTransition: Esses recursos permitem indicar que certas atualizações não são urgentes. Por exemplo, ao digitar em um campo de busca, queremos mostrar imediatamente o caractere novo, mas a pesquisa na API pode esperar um instante. O useTransition retorna [isPending, startTransition]. Use startTransition para envolver o setState de atualizações pesadas. Exemplo:

    import { useState, useTransition } from 'react'; function BuscaLenta() { const [texto, setTexto] = useState(""); const [resultados, setResultados] = useState([]); const [isPending, startTransition] = useTransition(); function handleChange(e) { const novoTexto = e.target.value; setTexto(novoTexto); // atualização urgente // Marca este bloco como transição (não urgente) startTransition(() => { fetch(`/api/search?q=${novoTexto}`) .then(res => res.json()) .then(dados => setResultados(dados)); }); } return ( <div> <input value={texto} onChange={handleChange} /> {isPending ? <p>Pesquisando...</p> : null} <ul> {resultados.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> ); }

    Neste código, setTexto(novoTexto) é urgente (o que aparece no input), enquanto a busca em si é iniciada dentro de startTransition. Se o usuário continuar digitando rápido, React pode cancelar pesquisas anteriores em andamento e priorizar a mais recente, mantendo o input responsivo. Como explicam documentações técnicas, em React 18 “atualizações não urgentes não bloqueiam a página, não importa quanto tempo levem” (3perf.com).

  • useDeferredValue: Esse hook é uma alternativa para adiar apenas a renderização de um valor. Por exemplo, em um filtro de lista, o que você digita é atualizado imediatamente, mas o filtro que depende disso pode esperar um pouco. Exemplo:

    import { useState, useDeferredValue, useMemo } from 'react'; function FiltroLento({ itens }) { const [filtro, setFiltro] = useState(""); const filtroAdiado = useDeferredValue(filtro); // Somente re-calcula a lista filtrada quando filtroAdiado muda const itensFiltrados = useMemo(() => { // Simula computação custosa return itens.filter(item => item.includes(filtroAdiado)); }, [itens, filtroAdiado]); return ( <div> <input value={filtro} onChange={e => setFiltro(e.target.value)} placeholder="Filtrar lista..." /> <ul> {itensFiltrados.map(item => ( <li key={item}>{item}</li> ))} </ul> </div> ); }

    Aqui, enquanto o usuário digita no input (que é urgente), o cálculo da lista só ocorrerá com o valor “adiado” depois de alguns milissegundos ociosos. Assim, a interface continua fluida, sem travar a cada caractere digitado.

  • Outros recursos de concorrência: O React 18 também melhora a hidratação de Suspense no servidor, introduz o hook useId para gerar IDs consistentes, e prepara o terreno para futuros componentes como <Offscreen> (que virá em atualizações futuras) para pré-carregar telas fora de vista.

Em resumo, esses recursos não apenas melhoram a performance bruta, mas muitas vezes elevam a performance percebida pelos usuários. Por exemplo, ao usar Transitions o usuário vê imediatamente a resposta aos seus cliques/digitações, mesmo que existam elementos demorados sendo atualizados por trás — evitando a sensação de lag.

Exemplos práticos: Suspense e Concurrent Mode funcionando juntos

Vamos ver um cenário completo juntando alguns conceitos. Suponha uma página que carrega dois componentes pesados: uma galeria de imagens e uma lista de amigos, ambos assíncronos. Queremos mostrar o conteúdo da galeria assim que pronto, sem atrasar pela lista de amigos. Podemos usar Suspense aninhados para ter dois fallbacks distintos:

import React, { Suspense, useState } from 'react'; import ReactDOM from 'react-dom/client'; const Feed = React.lazy(() => import('./Feed')); // carrega posts dos amigos const Gallery = React.lazy(() => import('./Gallery')); // carrega fotos function Dashboard() { return ( <div> <h1>Meu Dashboard</h1> <Suspense fallback={<Spinner message="Carregando galeria..." />}> <Gallery /> </Suspense> <Suspense fallback={<p>Carregando feed de amigos...</p>}> <Feed /> </Suspense> </div> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<Dashboard />);

Nesse exemplo, a galeria e o feed de amigos carregam quase em paralelo. A galeria é exibida assim que termina, mesmo que o feed ainda esteja carregando, graças ao segundo <Suspense>. Cada componente tem seu próprio fallback. O usuário vê conteúdo parcial logo: primeiro a interface, depois a galeria, depois o feed, sem tela em branco completa em nenhum momento.

Agora, imagine que ao carregar o feed de amigos fazemos uma busca demorada ou renderizamos conteúdo pesado. Podemos combinar isso com useTransition:

function Feed() { const [posts, setPosts] = useState([]); const [isPending, startTransition] = useTransition(); React.useEffect(() => { startTransition(() => { fetch('/api/friends/posts') .then(response => response.json()) .then(data => setPosts(data)); }); }, []); return ( <> {isPending && <p>Atualizando feed...</p>} {posts.map(post => ( <div key={post.id}>{post.text}</div> ))} </> ); }

Aqui, logo que o componente Feed monta, iniciamos a busca dentro de startTransition. O texto Atualizando feed... aparece enquanto houver pendência (isPending). Se o usuário interage (por exemplo, clicando em algum botão), o React vai interromper essa transição demorada e priorizar a resposta ao clique, apenas depois continuando a carregar o feed.

Além disso, como estamos usando createRoot, todas essas atualizações serão automaticalmente batched, evitando re-renderizações extras. Por exemplo, se vários estados forem atualizados em seguida (como em uma sequência de chamadas a setState), o React agrupa tudo em uma única renderização. Isso significa menor custo e interface sem piscadas.

Esses padrões — Suspense para dividir a carga, Transitions para priorizar atualizações e batching para evitar trabalho extra — tornam a aplicação visivelmente mais rápida e suave. Usuários percebem menos “flicker” e mais progresso contínuo. Como destaca a documentação, “mesmo apps que demoram muito a carregar código e dados podem mostrar o conteúdo importante o mais cedo possível (legacy.reactjs.org)”, graças à preparação paralela e incremental de renderização.

Conclusão

No React 18 aprendemos que não basta apenas carregar dados e componentes mais rápido; é preciso orquestrar o carregamento para que a experiência do usuário seja fluida. O Suspense e os novos recursos concorrentes do React 18 nos dão as ferramentas para isso. Com eles, podemos mostrar rapidamente placeholders ou conteúdo parcial enquanto continuamos trabalhando, em vez de bloquear toda a UI até que tudo esteja pronto.

Em resumo, para acelerar sua aplicação React 18:

  • Utilize React.lazy + <Suspense> para code splitting. Faça seus componentes assíncronos carregarem sob demanda, exibindo um fallback amigável entre o carregamento.
  • Se possível, adote frameworks ou bibliotecas de dados que suportem Suspense para requisições, puxando dados já em roteadores ou handlers (como Next.js, Relay, Remix). Isso quebra gargalos de dados em cascata.
  • Prefira ReactDOM.createRoot() no lugar de ReactDOM.render() para habilitar o modo concorrente e o batching automático. Assim, seu app começa a usar por baixo do capô o novo reconciliador.
  • Empregue transições via useTransition/startTransition em situações de atualização pesada (busca em listas grandes, renderizações complexas). Isso faz com que interações urgentes (click, digitação) não travem a aplicação.
  • Use useDeferredValue para priorizar inputs do usuário sobre cálculos computadorizados demorados. Dessa forma, a interface responde instantaneamente e só depois atualiza o restante.
  • Pense em dividir suas páginas em pedaços que podem carregar incrementalmente: exiba primeiro o conteúdo principal, adiando o secundário em segundo plano.

Em última análise, o React 18 deixa claro: uma boa experiência de usuário não depende apenas de tempo de carregamento final, mas de como equilibramos essa carga. Como dissemos, o React agora consegue preparar várias versões da UI simultaneamente, pausando, retomando ou cancelando tarefas conforme necessário (he.legacy.reactjs.org). Use essas possibilidades a seu favor, sempre monitorando a performance, e você terá interfaces muito mais responsivas.

Perspectivas Futuras: O ecossistema de React continua a evoluir. Em breve podemos esperar recursos ainda mais diretos para data fetching com Suspense (já experimentado no React 19), além de novos componentes de otimização. Enquanto isso, comece a explorar o que React 18 oferece: experimente Suspense em pequenas partes da sua app, teste useTransition em cenários de lista, e sentirá a diferença na praticidade. Os exemplos aqui apresentados são o ponto de partida — adapte-os à sua realidade!

Lembrete final: apesar de poderosos, esses recursos requerem alguma reestruturação de pensamento (por exemplo, não confunda render síncrono antigo com as prioridades concorrentes). Mas com prática eles trazem benefícios enormes. Com o tempo, usar Suspense e as transições será tão natural quanto usar state e props. Boa codificação e interfaces cada vez mais rápidas!

Quer aprender mais sobre Programação?

Você acabou de ganhar 1 hora com a minha equipe para uma call exclusiva! Vamos entender o seu momento e te mostrar o caminho para se tornar um programador de sucesso. Clique no botão abaixo e agende agora mesmo!