Você já construiu um tooltip que dá um salto do canto superior esquerdo para o seu lugar correto? Ou um modal que pisca no tamanho errado antes de se ajustar? Esse glitch de fração de segundo é um flicker de layout. Isso acontece quando o React lê o DOM, calcula uma correção e atualiza o estado, mas o navegador já começou a renderizar os pixels na tela. A prescrição usual é trocar useEffect por useLayoutEffect. A troca funciona, mas apenas se você entender exatamente quando cada hook é disparado dentro do pipeline do navegador.
O Pipeline do Navegador: Render, Commit, Paint
O React atualiza um componente em três estágios distintos. Na fase de render, o React constrói — ou reconstrói — o Virtual DOM e computa o diff. Ainda não há mudanças reais de pixels; isso é pura computação ocorrendo na memória. Em seguida, vem a fase de commit, onde o React aplica essas mudanças aos nós reais do DOM. Estilos são atualizados, nós são inseridos ou removidos, e textos são alterados.
Então o navegador assume o controle. Na fase de paint, o mecanismo de renderização do navegador calcula a geometria do layout e desenha os pixels na tela. Essa sequência é rígida. O navegador deve completar o layout antes de poder fazer o paint, e deve terminar o paint antes que o usuário veja qualquer coisa nova. A lacuna entre o commit e o paint é medida em milissegundos, mas ela é real, e é a janela onde o useEffect e o useLayoutEffect divergem.
Por que o useEffect causa o flicker
O useEffect é executado de forma assíncrona, agendado para disparar após o navegador já ter feito o paint da tela. O DOM é atualizado, os pixels são desenhados e, então, o React entra em cena para executar o seu effect.
Imagine que você renderiza um menu dropdown sob um botão. Dentro do useEffect, você chama buttonRef.current.getBoundingClientRect(), computa as coordenadas corretas de top e left e as armazena no estado. Como o useEffect roda após o paint, o navegador já desenhou o dropdown em sua posição padrão, talvez em top: 0, left: 0. Somente após esse paint o seu effect atualiza o estado. O React faz o commit das coordenadas corrigidas e o navegador faz o paint novamente. O usuário vê dois frames: a posição errada e, em seguida, a correta. Esse salto visual é o flicker que todos tentam evitar.
Para busca de dados (data fetching), chamadas de API, rastreamento de analytics ou configuração de event listeners, esse atraso não importa. O usuário não se importa se um beacon de analytics dispara alguns milissegundos após o paint. Na verdade, adiar o trabalho não visual para depois do paint mantém a renderização inicial responsiva. Mas para correções dependentes de layout, o useEffect é simplesmente tarde demais.
Como o useLayoutEffect bloqueia o paint
O useLayoutEffect é executado de forma síncrona, imediatamente após o React mutar o DOM, mas antes que o navegador tenha a chance de calcular o layout ou desenhar os pixels. Ele bloqueia o pipeline de paint inteiramente.
Se você realizar a mesma medição do dropdown dentro do useLayoutEffect, a sequência muda. O React faz o commit da atualização inicial do DOM, executa o seu layout effect, e a atualização do seu estado dispara uma re-renderização síncrona. O React faz o commit das coordenadas corrigidas e só então o navegador faz o paint. O usuário vê apenas um frame, já correto.
Esse comportamento de bloqueio é tanto o recurso quanto o risco. Como o useLayoutEffect impede o navegador de fazer o paint até que ele termine, qualquer computação pesada dentro dele congela a UI. Mesmo algumas dezenas de milissegundos de paint bloqueado parecem jank para o usuário. É por isso que a documentação do React diz explicitamente para você começar com useEffect e só mudar para useLayoutEffect quando você realmente observar um flicker que não possa tolerar.
Quando usar cada hook
A maior parte da sua lógica pertence ao useEffect. Use-o para:
- Buscar dados de uma API
- Configurar inscrições (subscriptions) ou event listeners
- Enviar eventos de analytics
- Qualquer efeito colateral que não leia ou mude o layout imediatamente
Reserve o useLayoutEffect para operações que devem ler o DOM e escrever de volta antes que o usuário veja o frame:
- Medir dimensões de elementos, como largura, altura ou posição de scroll
- Calcular coordenadas para tooltips, popovers ou menus de contexto
- Prevenir deslocamentos de layout (layout shifts) visíveis quando a posição visual depende da geometria renderizada
Se não tiver certeza de qual escolher, use o useEffect por padrão. Mude para o useLayoutEffect apenas quando notar instabilidade visual. Só essa regra manterá a grande maioria das aplicações React funcionando perfeitamente.
A armadilha do Server-Side Rendering
If you use Next.js, Remix, or any framework that renders React on the server, you will hit a warning with useLayoutEffect. Because the server has no DOM, the hook has nothing to measure. React warns you that it expected a browser environment and did not find one. During hydration, this mismatch can also cause subtle bugs because the server-rendered markup and the client’s first intended render may differ.
The standard fix is an isomorphic hook that selects the right effect based on the environment:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
Use this wrapper in any component that must measure DOM nodes but might execute during server rendering. It silences the warning and keeps your server output consistent.
Performance and Best Practices
Since useLayoutEffect blocks painting, keep the body of the hook as light as possible. Read the layout value, compute the correction, and write it back. Do not fetch data, parse large objects, or run expensive algorithms inside it. Heavy code here will stall the main thread and make your interface feel frozen.
When you measure elements, use React refs rather than document.getElementById. Refs are tied to your component instance, survive re-renders without query tricks, and work reliably with portals or conditional rendering. Global ID lookups break component encapsulation and can return null at exactly the moment you need them.
useEffect is the right default for nearly every side effect. It lets the browser paint without interruption and handles data, events, and external synchronization cleanly. useLayoutEffect is a specialized tool for a specific problem: reading layout and writing back before the paint. Master the timing difference between them, and you will stop chasing flickers and start preventing them.
The real takeaway: Start with useEffect for everything. The moment you see a tooltip or modal blink into the wrong place before correcting itself, that is your signal. Switch to useLayoutEffect, measure the DOM, adjust your layout, and let the browser paint once—correctly.
