React Compiler: adeus useMemo e useCallback manual?
1. Visão geral
Muitos projetos React têm trechos de useMemo, useCallback e React.memo espalhados por toda a codebase. O objetivo é evitar re-renders desnecessários, mas o preço é um código mais longo e propenso a erros de dependência. React Compiler move essa memoização para o tempo de build: ele analisa components e injeta cache automaticamente onde necessário.
Este artigo explica o que é React Compiler e por que está recebendo atenção. Você saberá como ativar o compilador em um projeto Vite, como migrar gradualmente, como escrever testes para o comportamento de memoização e quando evitar usar.
2. O problema com memoização manual
Xet uma página de lista de pedidos com um campo de filtro e um contador não relacionado:
import { useState, useMemo, useCallback, memo } from "react";
type Order = { id: number; customer: string; total: number };
const OrderList = memo(function OrderList({
orders,
onSelect,
}: {
orders: Order[];
onSelect: (id: number) => void;
}) {
return (
<ul>
{orders.map((o) => (
<li key={o.id} onClick={() => onSelect(o.id)}>
{o.customer}: {o.total}
</li>
))}
</ul>
);
});
export function OrdersPage({ orders }: { orders: Order[] }) {
const [keyword, setKeyword] = useState("");
const [clicks, setClicks] = useState(0);
const filtered = useMemo(
() => orders.filter((o) => o.customer.includes(keyword)),
[orders, keyword]
);
const handleSelect = useCallback((id: number) => {
console.log("Selected order", id);
}, []);
return (
<>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={() => setClicks((c) => c + 1)}>Clicks: {clicks}</button>
<OrderList orders={filtered} onSelect={handleSelect} />
</>
);
}
O código funciona corretamente, mas tem três pontos frágeis:
- Se você remover
memodeOrderList, ambosuseMemoeuseCallbackse tornam praticamente inúteis. - Se você esquecer
keywordno array de dependência, a lista filtrada exibirá dados antigos. - Basta uma nova prop que seja um literal de objeto como
style={{ color: "red" }}, e todo o esforço de memoização é quebrado.
Memoização manual é um sistema implícito. Qualquer um que modifique o component precisa entendê-lo, e erros geralmente não aparecem até que alguém perfil o desempenho.
3. O que é React Compiler
React Compiler é uma ferramenta que roda no tempo de build, geralmente como um plugin do Babel. Ele lê o código de components e hooks, entende o fluxo de dados e então gera código com cache para:
- Valores computados: equivalente a
useMemo. - Funções: equivalente a
useCallback. - Elementos JSX: equivalente a envolver children com
memo. Se os inputs de um elemento não mudarem, o compilador reutiliza o elemento antigo e o React pula o re-render daquela subárvore.
O trecho abaixo ilustra a ideia do código após a compilação. Esta é uma versão simplificada, não o output real:
function OrdersPage({ orders }) {
const $ = useMemoCache(6); // array de cache anexado à instância do component
const [keyword, setKeyword] = useState("");
let filtered;
if ($[0] !== orders || $[1] !== keyword) {
filtered = orders.filter((o) => o.customer.includes(keyword));
$[0] = orders;
$[1] = keyword;
$[2] = filtered;
} else {
filtered = $[2];
}
// ... similar para handleSelect e elementos JSX
}
O compilador compara cada input com o valor do render anterior. Apenas quando um input muda, ele recalcula. Comparado com memoização manual, existem duas diferenças importantes:
- Mais granular: o compilador pode memoizar até partes que vêm após instruções condicionais, coisa que
useMemonão consegue fazer porque hooks não podem ser chamados condicionalmente. - Sem arrays de dependência manual: as dependências são inferidas do código, então não é possível “esquecer”.
4. Por que React Compiler está recebendo atenção
Existem várias razões pelas quais este tópico está recebendo muita atenção:
- Menos boilerplate: o código volta à forma “React puro”, mais fácil de ler e revisar.
- Melhor desempenho por padrão: o time não precisa de um especialista em profiling para evitar re-renders desnecessários.
- Força a conformidade com as Rules of React: o compilador só consegue otimizar código que segue as regras. Portanto, ele vem com um linter que ajuda a detectar violações, mesmo se você ainda não tiver ativado o compilador.
- Adoção gradual: pode ser ativado em um diretório ou component, sem precisar reescrever a aplicação inteira.
Nota: O compilador não torna o código mais rápido em todos os lugares. Ele reduz o trabalho de re-render. Se o gargalo for network, tamanho do bundle ou um loop pesado que roda toda vez que o input muda, o compilador não resolve.
5. Ativar React Compiler no projeto
5.1. Instalação com Vite
Instale o plugin e o linter. O nome do pacote pode mudar ao longo do tempo, então verifique a documentação oficial do React para a versão que está usando.
npm install -D babel-plugin-react-compiler eslint-plugin-react-hooks
Adicione o plugin à configuração do Vite:
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
babel: {
plugins: [["babel-plugin-react-compiler", {}]],
},
}),
],
});
O compilador deve rodar primeiro na cadeia de plugins do Babel, pois precisa ler o código original antes de outros plugins o transformarem.
Se o projeto ainda usar uma versão mais antiga do React, o compilador precisa de um pacote de runtime adicional e da opção target para gerar código compatível. Essa parte depende da versão, então consulte a documentação antes de configurar.
Com frameworks como Next.js ou Expo, geralmente há flags de configuração específicas para ativar o compilador. Os nomes dessas flags mudaram entre versões, então leia o release notes do framework em vez de copiar configurações antigas.
5.2. Verificar se o compilador está funcionando
Abra o React DevTools. Components otimizados pelo compilador terão um rótulo do tipo “Memo ✨” ao lado do nome. Sem esse rótulo significa que o compilador pulou o component, geralmente por violar uma regra da seção 6.
5.3. Ativar linting
// eslint.config.js
import reactHooks from "eslint-plugin-react-hooks";
export default [
reactHooks.configs.recommended,
];
O linter avisa sobre trechos de código que o compilador pulará ou pode executar incorretamente. Você deve ativar o linter antes do compilador e corrigir todos os avisos.
6. Rules of React: condições para o compilador funcionar
O compilador assume que o component é uma função pura durante o render. O exemplo abaixo viola isso:
export function CartSummary({ items }: { items: { price: number }[] }) {
items.sort((a, b) => b.price - a.price); // mutar prop!
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>Top item: {items[0]?.price}, total: {total}</p>;
}
items.sort modifica diretamente o array passado. Com cache, o compilador pode não perceber que items “mudou” porque a referência permanece a mesma, e a UI exibe informações incorretas. O linter avisará algo como:
error Mutating a value returned from a function whose return value should not be mutated react-hooks/...
A mensagem específica varia conforme a versão do plugin. A solução é criar uma cópia:
const sorted = [...items].sort((a, b) => b.price - a.price);
Violações comuns:
- Mutar props, state ou valores retornados de hooks.
- Ler ou escrever
ref.currentdurante o render. - Chamar hooks condicionalmente ou dentro de loops.
- Efeitos colaterais diretos no corpo do component, como escrever em variáveis de módulo ou chamar APIs.
Quando encontra código assim, o compilador geralmente pula o component em vez de quebrá-lo. Este é um comportamento seguro, mas você perde a otimização.
7. Migração gradual em projetos grandes
7.1. Modo opt-in
Com uma codebase grande, você pode compilar apenas components marcados:
// vite.config.js (trecho)
plugins: [["babel-plugin-react-compiler", { compilationMode: "annotation" }]],
export function InvoiceTable({ invoices }: { invoices: Invoice[] }) {
"use memo";
// ... este component é compilado
}
A diretiva "use memo" no início do corpo da função diz ao compilador para processar este component. Outros components permanecem inalterados.
7.2. Excluir temporariamente um component
Quando ativado globalmente, mas um component causa erro, você pode desativar temporariamente o compilador apenas para ele:
export function LegacyChart(props: ChartProps) {
"use no memo";
// ... código antigo que usa bibliotecas que mutam dados
}
Nota:
"use no memo"é um escape temporário. Você deve anotar o motivo e criar uma tarefa para corrigir, não deixe isso se tornar o padrão.
7.3. E quanto aos useMemo antigos?
O compilador funciona com código que já tem useMemo/useCallback, então você não precisa deletar tudo de uma vez. Deletar em massa pode mudar comportamentos sutis, especialmente quando o valor é usado como dependência de um useEffect. Remova gradualmente, com testes. Para novos components, simplesmente escreva código puro e deixe o compilador cuidar.
8. Escrever testes para comportamento de memoização
Você pode verificar que o child não re-renderiza quando um state não relacionado muda. O exemplo abaixo usa Vitest e React Testing Library, rodando com a mesma configuração do Vite com compilador ativado:
npm install -D vitest @testing-library/react @testing-library/user-event jsdom
// OrdersPage.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, it, expect, vi } from "vitest";
const renderSpy = vi.fn();
function OrderList({ orders }: { orders: string[] }) {
renderSpy();
return <ul>{orders.map((o) => <li key={o}>{o}</li>)}</ul>;
}
function OrdersPage() {
const [clicks, setClicks] = useState(0);
const orders = ["Alice", "Bob"]; // sem useMemo
return (
<>
<button onClick={() => setClicks((c) => c + 1)}>Clicks: {clicks}</button>
<OrderList orders={orders} />
</>
);
}
describe("OrdersPage com React Compiler", () => {
it("não re-renderiza OrderList quando apenas o contador muda", async () => {
render(<OrdersPage />);
await userEvent.click(screen.getByRole("button"));
await userEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent("Clicks: 2");
expect(renderSpy).toHaveBeenCalledTimes(1);
});
});
Explicação:
ordersé um array literal semuseMemo. Sem o compilador, um novo array é criado a cada render eOrderListre-renderiza.- Com o compilador, tanto o array
ordersquanto o elemento<OrderList />são cacheados porque não dependem declicks. renderSpyé chamado apenas uma vez, no primeiro render.
Resultado esperado:
✓ OrdersPage com React Compiler > não re-renderiza OrderList quando apenas o contador muda
Test Files 1 passed (1)
Se desativar o compilador, a asserção final falhará com valor 3. O matcher toHaveTextContent requer @testing-library/jest-dom no arquivo de configuração.
Nota: Chamar spy no corpo do component é um efeito colateral. Essa abordagem é aceitável apenas em testes para medir quantas vezes um render ocorre. Não aplique isso no código de produção.
9. Quando NÃO usar (ou ser cauteloso)
- Codebase que viola Rules of React bastante: o compilador pulará a maioria dos components, ou pior, pode expor bugs que estavam sendo mascarados. Corrija de acordo com o lint primeiro.
- Bibliotecas baseadas em mutação: algumas bibliotecas de state ou form mutam objetos no local e esperam que o component leia o novo valor em cada render. Para components que as usam, pode ser necessário
"use no memo". - Pipeline de build customizado: se você não usar Babel ou o toolchain não suportar plugins, a integração custará caro. Verifique o suporte do bundler antes.
- Aplicações pequenas com pouca interatividade: o benefício pode não ser significativo em comparação com o tempo de build adicional e complexidade de debug.
- Precisa garantir referências estáveis para effects: o compilador memoiza para otimizar, não para prometer semântica. Se a lógica correta/incorreta depende de uma referência permanecer estável, por exemplo para evitar que um effect seja executado novamente, mantenha
useMemo/useCallbackexplícito para comunicar a intenção.
Além disso, ao fazer debug, o código executado no navegador é diferente do que você escreveu. Source maps ajudam, mas você deve se familiarizar com DevTools e as ferramentas de playground do compilador para ver o output quando necessário.
10. Conclusão
React Compiler não elimina useMemo e useCallback, mas os transforma de ferramentas de uso diário em escapes para casos especiais. O pré-requisito é que o código obedeça às Rules of React. Portanto, o primeiro passo é sempre ativar o linter e corrigir as violações.
Roteiro sugerido: ative o linter, corrija avisos, ative o compilador em modo annotation para alguns components, escreva testes medindo quantas vezes ocorrem renders, depois expanda gradualmente. Fazendo assim, você obtém melhor desempenho sem precisar de arrays de dependência escritos manualmente.