Aprofunde-se na Renderização Concorrente, Suspense e Transições do React. Otimize o desempenho e entregue experiências fluidas com recursos avançados do React 18.
Renderização Concorrente do React: Dominando a Otimização de Suspense e Transições para Experiências de Usuário Aprimoradas
No cenário dinâmico do desenvolvimento web, a experiência do usuário (UX) reina soberana. As aplicações devem ser responsivas, interativas e visualmente fluidas, independentemente das condições de rede, capacidades do dispositivo ou da complexidade dos dados processados. Por anos, o React tem capacitado desenvolvedores a construir interfaces de usuário sofisticadas, mas os padrões de renderização tradicionais às vezes podiam levar a uma experiência "travada" ou de congelamento quando ocorriam computações pesadas ou buscas de dados.
Entre na Renderização Concorrente do React. Essa mudança de paradigma, introduzida completamente no React 18, representa uma re-arquitetura fundamental do mecanismo de renderização central do React. Não é um novo conjunto de recursos que você ativa com uma única flag; em vez disso, é uma mudança subjacente que habilita novas capacidades como Suspense e Transições, que melhoram drasticamente como as aplicações React gerenciam a responsividade e o fluxo do usuário.
Este guia abrangente aprofundará a essência do React Concorrente, explorará seus princípios fundamentais e fornecerá insights acionáveis sobre como alavancar Suspense e Transições para construir aplicações verdadeiramente fluidas e performáticas para um público global.
Compreendendo a Necessidade do React Concorrente: O Problema do "Jank"
Antes do React Concorrente, a renderização do React era em grande parte síncrona e bloqueadora. Quando uma atualização de estado ocorria, o React começava imediatamente a renderizar essa atualização. Se a atualização envolvesse muito trabalho (por exemplo, re-renderizar uma grande árvore de componentes, realizar cálculos complexos ou esperar por dados), o thread principal do navegador ficaria ocupado. Isso poderia levar a:
- UI Não Responsiva: A aplicação pode congelar, tornar-se não responsiva à entrada do usuário (como cliques ou digitação), ou exibir conteúdo desatualizado enquanto o novo conteúdo carrega.
- Animações Gaguejantes: As animações podem parecer irregulares enquanto o navegador luta para manter 60 quadros por segundo.
- Má Percepção do Usuário: Os usuários percebem uma aplicação lenta e não confiável, levando a frustração e abandono.
Considere um cenário onde um usuário digita em um campo de busca. Tradicionalmente, cada tecla digitada pode desencadear uma re-renderização de uma grande lista. Se a lista for extensa ou a lógica de filtragem complexa, a UI pode ficar atrás da digitação do usuário, criando uma experiência desagradável. O React Concorrente visa resolver esses problemas tornando a renderização interrompível e priorizável.
O Que é React Concorrente? A Ideia Central
Em sua essência, o React Concorrente permite que o React trabalhe em múltiplas tarefas concomitantemente. Isso não significa paralelismo verdadeiro (que é tipicamente alcançado através de web workers ou múltiplos núcleos de CPU), mas sim que o React pode pausar, retomar e até mesmo abandonar o trabalho de renderização. Ele pode priorizar atualizações urgentes (como entrada do usuário) em detrimento de outras menos urgentes (como busca de dados em segundo plano).
Princípios chave do React Concorrente:
- Renderização Interrompível: O React pode começar a renderizar uma atualização, pausar se uma atualização mais urgente surgir (por exemplo, um clique do usuário), lidar com a atualização urgente e então retomar o trabalho pausado ou até mesmo descartá-lo se não for mais relevante.
- Priorização: Diferentes atualizações podem ter diferentes prioridades. A entrada do usuário (digitação, clique) é sempre de alta prioridade, enquanto o carregamento de dados em segundo plano ou a renderização fora da tela podem ter prioridade mais baixa.
- Atualizações Não Bloqueadoras: Como o React pode pausar o trabalho, ele evita bloquear o thread principal, garantindo que a UI permaneça responsiva.
- Agrupamento Automático (Automatic Batching): O React 18 agrupa múltiplas atualizações de estado em uma única re-renderização, mesmo fora dos manipuladores de eventos, o que reduz ainda mais renderizações desnecessárias e melhora o desempenho.
A beleza do React Concorrente é que grande parte dessa complexidade é tratada internamente pelo React. Os desenvolvedores interagem com ele através de novos padrões e hooks, principalmente Suspense e Transições.
Suspense: Gerenciando Operações Assíncronas e Fallbacks de UI
Suspense é um mecanismo que permite que seus componentes "esperem" por algo antes de renderizar. Em vez dos métodos tradicionais de lidar com estados de carregamento (por exemplo, definir manualmente flags `isLoading`), o Suspense permite que você defina declarativamente uma UI de fallback que será exibida enquanto um componente ou seus filhos estão carregando dados, código ou outros recursos de forma assíncrona.
Como o Suspense Funciona
Quando um componente dentro de um limite <Suspense>
"suspende" (por exemplo, ele lança uma promessa enquanto espera por dados), o React captura essa promessa e renderiza a prop fallback
do componente <Suspense>
mais próximo. Uma vez que a promessa é resolvida, o React tenta renderizar o componente novamente. Isso simplifica significativamente o tratamento de estados de carregamento, tornando seu código mais limpo e sua UX mais consistente.
Casos de Uso Comuns para Suspense:
1. Divisão de Código com React.lazy
Um dos casos de uso mais antigos e amplamente adotados para o Suspense é a divisão de código. React.lazy
permite que você adie o carregamento do código de um componente até que ele seja realmente renderizado. Isso é crucial para otimizar os tempos de carregamento inicial da página, especialmente para grandes aplicações com muitos recursos.
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function MyPage() {
return (
<div>
<h1>Bem-vindo à Minha Página</h1>
<Suspense fallback={<div>Carregando componente...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
Neste exemplo, o código de LazyComponent
só será buscado quando MyPage
tentar renderizá-lo. Até então, o usuário verá "Carregando componente...".
2. Busca de Dados com Suspense (Padrões Experimentais/Recomendados)
Enquanto `React.lazy` é embutido, suspender diretamente para a busca de dados requer uma integração com uma biblioteca de busca de dados habilitada para Suspense ou uma solução personalizada. A equipe do React recomenda o uso de frameworks ou bibliotecas opiniáticas que se integram com o Suspense para a busca de dados, como Relay ou Next.js com seus novos padrões de busca de dados (por exemplo, `async` server components que transmitem dados). Para a busca de dados no lado do cliente, bibliotecas como SWR ou React Query estão evoluindo para suportar padrões de Suspense.
Um exemplo conceitual usando um padrão preparado para o futuro com o use
hook (disponível no React 18+ e amplamente utilizado em componentes de servidor):
import { Suspense, use } from 'react';
// Simula uma função de busca de dados que retorna uma Promise
const fetchData = async () => {
const response = await new Promise(resolve => setTimeout(() => {
resolve({ name: 'Usuário Global', role: 'Desenvolvedor' });
}, 2000));
return response;
};
let userDataPromise = fetchData();
function UserProfile() {
// O hook `use` lê o valor de uma Promise. Se a Promise estiver pendente,
// ele suspende o componente.
const user = use(userDataPromise);
return (
<div>
<h3>Perfil do Usuário</h3>
<p>Nome: <b>{user.name}</b></p>
<p>Cargo: <em>{user.role}</em></p>
</div>
);
}
function App() {
return (
<div>
<h1>Painel de Controle da Aplicação</h1>
<Suspense fallback={<div>Carregando perfil do usuário...</div>}>
<UserProfile />
</Suspense>
</div>
);
}
O hook `use` é uma nova primitiva poderosa para ler valores de recursos como Promises na renderização. Quando `userDataPromise` está pendente, `UserProfile` suspende, e o limite `Suspense` exibe seu fallback.
3. Carregamento de Imagem com Suspense (Bibliotecas de Terceiros)
Para imagens, você pode usar uma biblioteca que envolve o carregamento de imagens de forma compatível com Suspense, ou criar seu próprio componente que lança uma promessa até que a imagem seja carregada.
Limites de Suspense Aninhados
Você pode aninhar limites <Suspense>
para fornecer estados de carregamento mais granulares. O fallback do limite de Suspense mais interno será mostrado primeiro, depois substituído pelo conteúdo resolvido, potencialmente revelando o próximo fallback externo, e assim por diante. Isso permite um controle preciso sobre a experiência de carregamento.
<Suspense fallback={<div>Carregando Página...</div>}>
<HomePage />
<Suspense fallback={<div>Carregando Widgets...</div>}>
<DashboardWidgets />
</Suspense>
</Suspense>
Limites de Erro com Suspense
O Suspense lida com estados de carregamento, mas não lida com erros. Para erros, você ainda precisa de Limites de Erro. Um Limite de Erro é um componente React que captura erros JavaScript em qualquer lugar de sua árvore de componentes filhos, registra esses erros e exibe uma UI de fallback em vez de travar toda a aplicação. É uma boa prática envolver os limites de Suspense com Limites de Erro para capturar problemas potenciais durante a busca de dados ou o carregamento de componentes.
import { Suspense, lazy, Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("Ocorreu um erro:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Algo deu errado ao carregar este conteúdo.</h2>;
}
return this.props.children;
}
}
const LazyDataComponent = lazy(() => new Promise(resolve => {
// Simula um erro 50% das vezes
if (Math.random() > 0.5) {
throw new Error("Falha ao carregar dados!");
} else {
setTimeout(() => resolve({ default: () => <p>Dados carregados com sucesso!</p> }), 1000);
}
}));
function DataDisplay() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Carregando dados...</div>}>
<LazyDataComponent />
</Suspense>
</ErrorBoundary>
);
}
Transições: Mantendo a UI Responsiva Durante Atualizações Não Urgentes
Enquanto o Suspense aborda o problema de "esperar que algo carregue", as Transições abordam o problema de "manter a UI responsiva durante atualizações complexas". As Transições permitem que você marque certas atualizações de estado como "não urgentes". Isso sinaliza ao React que, se uma atualização urgente (como entrada do usuário) ocorrer enquanto a atualização não urgente está sendo renderizada, o React deve priorizar a urgente e potencialmente descartar a renderização não urgente em andamento.
O Problema Que as Transições Resolvem
Imagine uma barra de busca que filtra um grande conjunto de dados. Conforme o usuário digita, um novo filtro é aplicado e a lista é re-renderizada. Se a re-renderização for lenta, o próprio campo de busca pode se tornar lento, tornando a experiência do usuário frustrante. A digitação (urgente) é bloqueada pela filtragem (não urgente).
Apresentando startTransition
e useTransition
O React oferece duas maneiras de marcar atualizações como transições:
startTransition(callback)
: Uma função autônoma que você pode importar do React. Ela envolve atualizações que você deseja que sejam tratadas como transições.useTransition()
: Um Hook do React que retorna um array contendo um booleanoisPending
(indicando se uma transição está ativa) e uma funçãostartTransition
. Isso é geralmente preferido dentro de componentes.
Como as Transições Funcionam
Quando uma atualização é envolvida em uma transição, o React a lida de forma diferente:
- Ele renderizará as atualizações da transição em segundo plano sem bloquear o thread principal.
- Se uma atualização mais urgente (como digitar em um campo de entrada) ocorrer durante uma transição, o React interromperá a transição, processará a atualização urgente imediatamente e, em seguida, reiniciará ou abandonará a transição.
- O estado `isPending` de `useTransition` permite que você mostre um indicador de pendência (por exemplo, um spinner ou estado esmaecido) enquanto a transição está em andamento, dando feedback visual ao usuário.
Exemplo Prático: Lista Filtrada com useTransition
import React, { useState, useTransition } from 'react';
const DATA_SIZE = 10000;
const generateData = () => {
return Array.from({ length: DATA_SIZE }, (_, i) => `Item ${i + 1}`);
};
const allItems = generateData();
function FilterableList() {
const [inputValue, setInputValue] = useState('');
const [displayValue, setDisplayValue] = useState('');
const [isPending, startTransition] = useTransition();
const filteredItems = React.useMemo(() => {
if (!displayValue) return allItems;
return allItems.filter(item =>
item.toLowerCase().includes(displayValue.toLowerCase())
);
}, [displayValue]);
const handleChange = (e) => {
const newValue = e.target.value;
setInputValue(newValue); // Atualização urgente: atualiza o input imediatamente
// Atualização não urgente: inicia uma transição para filtrar a lista
startTransition(() => {
setDisplayValue(newValue);
});
};
return (
<div>
<h2>Buscar e Filtrar</h2>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Digite para filtrar..."
style={{ width: '100%', padding: '8px', marginBottom: '10px' }}
/>
{isPending && <div style={{ color: 'blue' }}>Atualizando lista...</div>}
<ul style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
function App() {
return (
<div>
<h1>Exemplo de Transições Concorrentes do React</h1>
<FilterableList />
</div>
);
}
Neste exemplo:
- Digitar no campo de entrada atualiza
inputValue
imediatamente, mantendo o campo responsivo. Esta é uma atualização urgente. - O
startTransition
envolve a atualização desetDisplayValue
. Isso informa ao React que a atualização da lista exibida é uma tarefa não urgente. - Se o usuário digitar rapidamente, o React pode interromper a filtragem da lista, atualizar o campo de entrada e, em seguida, reiniciar o processo de filtragem, garantindo uma experiência de digitação suave.
- A flag
isPending
fornece feedback visual de que a lista está sendo atualizada.
Quando Usar Transições
Use transições quando:
- Uma atualização de estado pode levar a uma re-renderização significativa e potencialmente lenta.
- Você deseja manter a UI responsiva para interações imediatas do usuário (como digitação) enquanto uma atualização mais lenta e não crítica ocorre em segundo plano.
- O usuário não precisa ver os estados intermediários da atualização mais lenta.
NÃO use transições para:
- Atualizações urgentes que devem ser imediatas (por exemplo, alternar uma caixa de seleção, feedback de envio de formulário).
- Animações que exigem tempo preciso.
useDeferredValue
: Adiando Atualizações para Melhor Responsividade
O Hook useDeferredValue
está intimamente relacionado às transições e oferece outra maneira de manter a UI responsiva. Ele permite que você adie a atualização de um valor, assim como `startTransition` adia uma atualização de estado. Se o valor original mudar rapidamente, `useDeferredValue` retornará o valor *anterior* até que uma versão "estável" do novo valor esteja pronta, evitando que a UI congele.
Como useDeferredValue
Funciona
Ele recebe um valor e retorna uma versão "adiada" desse valor. Quando o valor original muda, o React tenta atualizar o valor adiado de forma não bloqueadora e de baixa prioridade. Se outras atualizações urgentes ocorrerem, o React pode atrasar a atualização do valor adiado. Isso é particularmente útil para coisas como resultados de busca ou gráficos dinâmicos, onde você deseja mostrar a entrada imediata, mas atualizar a exibição cara somente depois que o usuário pausar ou a computação for concluída.
Exemplo Prático: Entrada de Busca Adiada
import React, { useState, useDeferredValue } from 'react';
const ITEMS = Array.from({ length: 10000 }, (_, i) => `Produto ${i + 1}`);
function DeferredSearchList() {
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm); // Versão adiada de searchTerm
// Esta operação de filtro cara usará o deferredSearchTerm
const filteredItems = React.useMemo(() => {
// Simula um cálculo pesado
for (let i = 0; i < 500000; i++) {}
return ITEMS.filter(item =>
item.toLowerCase().includes(deferredSearchTerm.toLowerCase())
);
}, [deferredSearchTerm]);
const handleChange = (e) => {
setSearchTerm(e.target.value);
};
return (
<div>
<h2>Exemplo de Busca Adiada</h2>
<input
type="text"
value={searchTerm}
onChange={handleChange}
placeholder="Buscar produtos..."
style={{ width: '100%', padding: '8px', marginBottom: '10px' }}
/>
{searchTerm !== deferredSearchTerm && <div style={{ color: 'green' }}>Buscando...</div>}
<ul style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
function App() {
return (
<div>
<h1>Exemplo de useDeferredValue do React</h1>
<DeferredSearchList />
</div>
);
}
Neste exemplo:
- O campo de entrada é atualizado imediatamente conforme o usuário digita porque
searchTerm
é atualizado diretamente. - A lógica de filtragem cara usa
deferredSearchTerm
. Se o usuário digitar rapidamente,deferredSearchTerm
ficará atrasado em relação asearchTerm
, permitindo que o campo de entrada permaneça responsivo enquanto a filtragem é feita em segundo plano. - Uma mensagem "Buscando..." é exibida quando `searchTerm` e `deferredSearchTerm` estão dessincronizados, indicando que a exibição está sendo atualizada.
useTransition
vs. useDeferredValue
Embora semelhantes em propósito, eles têm casos de uso distintos:
useTransition
: Usado quando você mesmo está causando a atualização lenta (por exemplo, definindo uma variável de estado que dispara uma renderização pesada). Você marca explicitamente a atualização como uma transição.useDeferredValue
: Usado quando uma prop ou variável de estado vem de uma fonte externa ou de um nível superior na árvore de componentes, e você deseja adiar seu impacto em uma parte cara do seu componente. Você adia o *valor*, não a atualização.
Melhores Práticas Gerais para Renderização Concorrente e Otimização
Adotar recursos concorrentes não se trata apenas de usar novos hooks; trata-se de mudar sua mentalidade sobre como o React gerencia a renderização e como melhor estruturar sua aplicação para desempenho e experiência do usuário ideais.
1. Adote o Strict Mode
O <StrictMode>
do React é inestimável ao trabalhar com recursos concorrentes. Ele intencionalmente invoca duas vezes certas funções (como `render` methods ou `useEffect` cleanup) no modo de desenvolvimento. Isso ajuda a detectar efeitos colaterais acidentais que poderiam causar problemas em cenários concorrentes onde os componentes podem ser renderizados, pausados e retomados, ou mesmo renderizados múltiplas vezes antes de serem confirmados no DOM.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
2. Mantenha os Componentes Puros e Isole os Efeitos Colaterais
Para que a renderização concorrente do React funcione de forma eficaz, seus componentes devem ser, idealmente, funções puras de suas props e estado. Evite efeitos colaterais em funções de renderização. Se a lógica de renderização do seu componente tiver efeitos colaterais, esses efeitos colaterais podem ser executados várias vezes ou descartados, levando a um comportamento imprevisível. Mova os efeitos colaterais para `useEffect` ou manipuladores de eventos.
3. Otimize Computações Caras com useMemo
e useCallback
Embora os recursos concorrentes ajudem a gerenciar a responsividade, eles não eliminam o custo da renderização. Use `useMemo` para memorizar cálculos caros e `useCallback` para memorizar funções passadas para componentes filhos. Isso evita re-renderizações desnecessárias de componentes filhos quando props ou funções não foram realmente alteradas.
function MyComponent({ data }) {
const processedData = React.useMemo(() => {
// Computação cara nos dados
return data.map(item => item.toUpperCase());
}, [data]);
const handleClick = React.useCallback(() => {
console.log('Botão clicado');
}, []);
return (
<div>
<p>{processedData.join(', ')}</p>
<button onClick={handleClick}>Clique em Mim</button>
</div>
);
}
4. Aproveite a Divisão de Código
Conforme demonstrado com `React.lazy` e `Suspense`, a divisão de código é uma poderosa técnica de otimização. Ela reduz o tamanho inicial do bundle, permitindo que sua aplicação carregue mais rapidamente. Divida sua aplicação em pedaços lógicos (por exemplo, por rota, por recurso) e carregue-os sob demanda.
5. Otimize as Estratégias de Busca de Dados
Para busca de dados, considere padrões que se integram bem com o Suspense, como:
- Fetch-on-render (com Suspense): Como mostrado com o hook `use`, os componentes declaram suas necessidades de dados e suspendem até que os dados estejam disponíveis.
- Render-as-you-fetch: Comece a buscar dados cedo (por exemplo, em um manipulador de eventos ou roteador) antes de renderizar o componente que precisa deles. Passe a promessa diretamente para o componente, que então usa `use` ou uma biblioteca habilitada para Suspense para ler a partir dela. Isso evita o efeito cascata e torna os dados disponíveis mais cedo.
- Server Components (Avançado): Para aplicações renderizadas no servidor, os React Server Components (RSC) se integram profundamente com o React Concorrente e o Suspense para transmitir HTML e dados do servidor, melhorando o desempenho de carregamento inicial e simplificando a lógica de busca de dados.
6. Monitore e Perfile o Desempenho
Use as ferramentas de desenvolvimento do navegador (por exemplo, React DevTools Profiler, aba Performance do Chrome DevTools) para entender o comportamento de renderização da sua aplicação. Identifique gargalos e áreas onde os recursos concorrentes podem ser mais benéficos. Procure por tarefas longas no thread principal e animações travadas.
7. Divulgação Progressiva com Suspense
Em vez de mostrar um único spinner global, use limites de Suspense aninhados para revelar partes da UI conforme elas ficam prontas. Essa técnica, conhecida como Divulgação Progressiva, faz com que a aplicação pareça mais rápida e responsiva, pois os usuários podem interagir com as partes disponíveis enquanto outras carregam.
Considere um painel onde cada widget pode carregar seus dados independentemente:
<div className="dashboard-layout">
<Suspense fallback={<div>Carregando Cabeçalho...</div>}>
<Header />
</Suspense>
<div className="main-content">
<Suspense fallback={<div>Carregando Widget de Análises...</div>}>
<AnalyticsWidget />
</Suspense>
<Suspense fallback={<div>Carregando Notificações...</div>}>
<NotificationsWidget />
</Suspense>
</div>
</div>
Isso permite que o cabeçalho apareça primeiro, depois os widgets individuais, em vez de esperar que tudo carregue.
O Futuro e o Impacto do React Concorrente
O React Concorrente, Suspense e Transições não são apenas recursos isolados; eles são blocos de construção fundamentais para a próxima geração de aplicações React. Eles permitem uma maneira mais declarativa, robusta e performática de lidar com operações assíncronas e gerenciar a responsividade da UI. Essa mudança impacta profundamente como pensamos sobre:
- Arquitetura da Aplicação: Incentiva uma abordagem mais centrada em componentes para a busca de dados e estados de carregamento.
- Experiência do Usuário: Leva a UIs mais suaves e resilientes que se adaptam melhor a diversas condições de rede e dispositivo.
- Ergonomia do Desenvolvedor: Reduz o boilerplate associado a estados de carregamento manuais e lógica de renderização condicional.
- Renderização no Lado do Servidor (SSR) e Server Components: Recursos concorrentes são parte integrante dos avanços no SSR, permitindo o streaming de HTML e a hidratação seletiva, melhorando drasticamente as métricas de carregamento inicial da página, como o Largest Contentful Paint (LCP).
À medida que a web se torna mais interativa e intensiva em dados, a necessidade de recursos de renderização sofisticados só aumentará. O modelo de renderização concorrente do React o posiciona na vanguarda da entrega de experiências de usuário de ponta globalmente, permitindo que as aplicações pareçam instantâneas e fluidas, independentemente de onde os usuários estejam localizados ou qual dispositivo estejam usando.
Conclusão
A Renderização Concorrente do React, impulsionada por Suspense e Transições, marca um avanço significativo no desenvolvimento front-end. Ela capacita os desenvolvedores a construir interfaces de usuário altamente responsivas e fluidas, dando ao React a capacidade de interromper, pausar e priorizar o trabalho de renderização. Ao dominar esses conceitos e aplicar as melhores práticas descritas neste guia, você pode criar aplicações web que não apenas funcionam excepcionalmente, mas também proporcionam experiências agradáveis e contínuas para usuários em todo o mundo.
Abrace o poder do React concorrente e desbloqueie uma nova dimensão de desempenho e satisfação do usuário em seu próximo projeto.