Performance no React

Por Lucas Bittencourt · · Atualizado em

Sumário:

Como o React renderiza

No React, um único componente pode ter componentes aninhados. Quando um componente renderiza, todos os seus filhos são renderizados junto por padrão.

Um render acontece quando:

  • O state do componente muda
  • O componente pai renderiza
  • Um context que ele usa muda

O processo de atualização pode ser dividido em 2 partes:

  1. Render: o React chama os seus componentes e monta uma nova árvore de elementos (o "virtual DOM"), e então a compara com a árvore anterior usando um algoritmo de diffing chamado reconciliation
  2. Commit: o React aplica apenas as diferenças no DOM real

Sem a reconciliation, atualizar o DOM real a cada render seria extremamente lento. Mas tenha em mente: mesmo quando nada muda no DOM, o render em si (chamar os seus componentes) ainda tem um custo. A maioria das otimizações de performance no React consiste em pular renders desnecessários ou deixá-los mais baratos.

Leia mais sobre render e commit aqui

Memoização

É uma técnica que salva o valor de retorno de funções puras, evitando recálculos pesados. Funções puras são funções que não dependem de valores externos; portanto, sempre retornam o mesmo resultado para a mesma entrada.

Também se espera que os componentes React sejam puros: dadas as mesmas props e o mesmo state, eles devem retornar o mesmo JSX.

Igualdade referencial

Quando falamos em igualdade referencial, queremos dizer que precisamos garantir que os tipos por referência sejam iguais quando não mudaram.

Em JavaScript, temos tipos primitivos e tipos por referência.

  • Dois tipos primitivos idênticos são sempre iguais em uma comparação rasa (por exemplo, 25 === 25, 'Lucas' === 'Lucas', true === true).
  • Dois tipos por referência idênticos não são iguais em uma comparação rasa (por exemplo, { age: 25 } === { age: 25 }, ['Lucas'] === ['Lucas']). Portanto, se você os cria durante o render, eles são diferentes a cada render.

O React compara valores com Object.is (parecido com ===), então precisamos que esses tipos por referência (objetos, arrays e funções) mantenham a mesma referência entre renders nos seguintes cenários:

  • O array de dependências de useEffect, useMemo e useCallback
  • Props de componentes memoizados

React Compiler

O React Compiler (estável desde a v1.0, em 2025) é uma ferramenta de build que memoiza automaticamente os seus componentes e hooks. Ele analisa o seu código e aplica o equivalente a useMemo, useCallback e memo onde for necessário, com mais precisão do que fazer isso na mão.

Com Next.js, habilite-o no next.config.ts depois de instalar o babel-plugin-react-compiler:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactCompiler: true,
};

export default nextConfig;

Com o compiler habilitado, você pode escrever código simples e deixar que ele cuide da memoização:

function TodoList({ todos, filter }: TodoListProps) {
  // memoized automatically, no useMemo needed
  const visibleTodos = filterTodos(todos, filter);

  // stable reference automatically, no useCallback needed
  const handleClick = (id: string) => toggleTodo(id);

  return <List items={visibleTodos} onClick={handleClick} />;
}

O compiler só funciona com código que segue as Rules of React (componentes puros, sem mutação de props ou state durante o render). O plugin de ESLint eslint-plugin-react-hooks aponta código que quebra essas regras.

Mesmo com o compiler, é importante entender useMemo, useCallback e memo: você vai encontrá-los em codebases existentes, e eles continuam funcionando como escape hatches.

React Hook: useMemo

Sintaxe:

const memoizedValue = useMemo(() => expensiveFunction(), dependencies);

Você usa useMemo para memoizar valores, ajudando a evitar recálculos pesados ou a manter a igualdade referencial.

1. Evitando recálculos pesados

Não faça isso:

// this runs on every render
const filteredList = heavyFunction(myList);

Faça isso:

// this runs only when myList changes
const filteredList = useMemo(() => heavyFunction(myList), [myList]);

2. Mantendo a igualdade referencial

Não faça isso:

const person = { name: props.name, age: props.age };

// this runs on every render, because person is a new object every time
useEffect(() => {
  console.log(person);
}, [person]);

Faça isso:

const person = useMemo(
  () => ({ name: props.name, age: props.age }),
  [props.name, props.age]
);

// this runs only when person changes
useEffect(() => {
  console.log(person);
}, [person]);

React Hook: useCallback

Sintaxe:

const memoizedFunction = useCallback(functionToMemoize, dependencies);

Você usa useCallback para memoizar funções, o que permite manter a igualdade referencial.

Não faça isso:

const doSideEffect = () => setTimeout(console.log, 1000);

// this runs on every render, because doSideEffect is a new function every time
useEffect(() => {
  const timeout = doSideEffect();

  return () => clearTimeout(timeout);
}, [doSideEffect]);

Faça isso:

const doSideEffect = useCallback(() => setTimeout(console.log, 1000), []);

// this runs only once
useEffect(() => {
  const timeout = doSideEffect();

  return () => clearTimeout(timeout);
}, [doSideEffect]);

Com dependências:

const doSideEffect = useCallback(
  () => setTimeout(() => console.log(name), 1000),
  [name]
);

// this runs only when name changes
useEffect(() => {
  const timeout = doSideEffect();

  return () => clearTimeout(timeout);
}, [doSideEffect]);

React: memo

Sintaxe:

const MemoizedComponent = memo(Component, arePropsEqual?);

Como sabemos, quando um componente renderiza, todos os seus filhos são renderizados por padrão. Para pular o render de um componente cujas props não mudaram, podemos memoizá-lo com a função memo. Por padrão, ela compara cada prop com Object.is (uma comparação rasa).

function UserCard({ user, onSelect }: UserCardProps) {
  return <button onClick={() => onSelect(user.id)}>{user.name}</button>;
}

const MemoizedUserCard = memo(UserCard);

O memo só funciona se as props mantiverem a mesma referência. Se o pai passa um novo objeto ou função a cada render (por exemplo, onSelect={(id) => select(id)}), o MemoizedUserCard renderiza mesmo assim. É por isso que o memo costuma ser combinado com useMemo e useCallback (ou com o React Compiler).

Você pode passar uma função de comparação customizada. Ela deve retornar true quando as props forem iguais (para pular o render):

function arePropsEqual(prevProps: UserCardProps, nextProps: UserCardProps) {
  return prevProps.user.id === nextProps.user.id;
}

const MemoizedUserCard = memo(UserCard, arePropsEqual);

Por que devemos usar a propriedade key em loops?

Sintaxe:

{
  list.map((item) => <p key={item.id}>{item.text}</p>);
}

O atributo key ajuda o React a identificar quais itens mudaram, foram adicionados ou foram removidos entre renders. Com keys estáveis, o React consegue mover os elementos existentes em vez de recriá-los. A key deve ser um identificador único e estável, como um ID vindo dos seus dados.

Por que não devemos usar o index em loops?

Sintaxe:

{
  list.map((item, index) => <p key={index}>{item.text}</p>);
}

Quando um item é adicionado, removido ou reordenado, os índices mudam, então o React associa os elementos errados:

  • Performance: o React atualiza o conteúdo de vários elementos em vez de apenas movê-los.
  • Bugs: o state de um componente (por exemplo, o valor de um input ou um checkbox) fica com o índice, não com o item, então ele aparece no item errado depois de uma reordenação ou remoção.

Usar o index só é aceitável quando a lista é estática (nunca é reordenada, filtrada ou alterada). Evite também key={Math.random()}: isso cria uma nova key a cada render, então o React recria todos os elementos toda vez.

Resetando um componente com key

Você também pode usar key fora de loops. Quando a key muda, o React destrói o componente e cria um novo, resetando todo o seu state.

// the form state resets when another user is selected
<ProfileForm key={user.id} user={user} />

Evitando estados derivados

Estados derivados são estados criados a partir de outro state ou de props. Eles precisam de effects para se manterem sincronizados, o que causa renders extras e bugs.

Não faça!

const [repos, setRepos] = useState<Repo[]>([]);
const [search, setSearch] = useState("");
const [filteredRepos, setFilteredRepos] = useState<Repo[]>([]);

// renders twice: once with the old filteredRepos, then again after the effect
useEffect(() => {
  setFilteredRepos(repos.filter((repo) => repo.name.includes(search)));
}, [search, repos]);

Faça!

const [repos, setRepos] = useState<Repo[]>([]);
const [search, setSearch] = useState("");

// calculated during the render
const filteredRepos = repos.filter((repo) => repo.name.includes(search));

Não precisamos de um state adicional derivado de outro state. Podemos simplesmente criar uma nova variável durante o render (e usar useMemo se o cálculo for pesado e você não usar o React Compiler).

You Might Not Need an Effect

Mantendo a UI responsiva: useTransition e useDeferredValue

Às vezes um render é pesado e não pode ser pulado (por exemplo, filtrar e renderizar uma lista grande a cada tecla digitada). Com o concurrent rendering, o React consegue marcar algumas atualizações como não urgentes e interrompê-las para lidar com as urgentes, como a digitação.

O useDeferredValue permite adiar a parte da UI que depende de um valor:

function SearchPage({ items }: SearchPageProps) {
  const [query, setQuery] = useState("");

  // the input updates immediately, the list updates when React has time
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <>
      <input value={query} onChange={(event) => setQuery(event.target.value)} />

      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <SlowList items={items} query={deferredQuery} />
      </div>
    </>
  );
}

O SlowList precisa ser memoizado (com memo ou com o React Compiler), caso contrário ele renderiza junto com o pai de qualquer forma.

O useTransition marca uma atualização de state como não urgente e te dá uma flag isPending:

function Tabs() {
  const [tab, setTab] = useState("home");
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab: string) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButtons onSelect={selectTab} isPending={isPending} />
      {tab === "home" && <Home />}
      {tab === "reports" && <SlowReports />}
    </>
  );
}

Prefira esses hooks a um debounce manual quando a parte lenta é o render. O debounce ainda é útil para reduzir requisições de rede.

Code splitting com lazy e Suspense

Quanto menos JavaScript o navegador baixa, faz parsing e executa, mais rápido a página carrega. Com lazy, um componente só é carregado quando é renderizado pela primeira vez.

import { lazy, Suspense } from "react";

// loaded only when the chart is shown
const Chart = lazy(() => import("./chart"));

function Dashboard({ showChart }: DashboardProps) {
  return (
    showChart && (
      <Suspense fallback={<p>Loading chart...</p>}>
        <Chart />
      </Suspense>
    )
  );
}

O Next.js já divide o seu código por rota e oferece o next/dynamic para o mesmo propósito.

Renderizando listas longas

Renderizar milhares de elementos no DOM é lento, não importa o quão bem você os memoize. Use virtualização: renderize apenas os itens visíveis na tela, com bibliotecas como o TanStack Virtual.

Para casos mais simples, a propriedade CSS content-visibility: auto permite que o navegador pule o trabalho de renderização dos elementos fora da tela.

Server Components

Com Server Components (por exemplo, o App Router do Next.js), os componentes rodam apenas no servidor, e o JavaScript deles nunca é enviado para o navegador.

  • Busque dados diretamente no servidor, perto do banco de dados, sem waterfalls no client.
  • Mantenha bibliotecas pesadas (por exemplo, parsers de markdown, syntax highlighters, bibliotecas de datas) no servidor.
  • Use "use client" apenas nos componentes que precisam de interatividade (state, effects, event handlers), e mantenha-os o menor possível e o mais fundo possível na árvore.

Trecho de código:

// Server Component: no JavaScript shipped for this component
export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <PostContent content={post.content} />

      {/* only this small component is sent to the browser */}
      <LikeButton postId={post.id} />
    </article>
  );
}

Meça antes de otimizar

Não chute, meça. A maioria dos componentes já é rápida o suficiente, e otimizações desnecessárias deixam o código mais difícil de ler.

  • Use a aba Profiler do React Developer Tools para ver quais componentes renderizam, por quê e quanto tempo levam.
  • Use o painel Performance do DevTools do navegador para encontrar long tasks.
  • Acompanhe as Core Web Vitals (LCP, INP, CLS) de usuários reais, por exemplo, com o Vercel Speed Insights.