Construindo Componentes Conscientes de Scroll em React
Seu manipulador de scroll dispara 60 vezes por segundo. Seu componente re-renderiza a cada tick. Seus usuários veem travamentos em vez de animações suaves. Este padrão quebra aplicações React mais rápido do que quase qualquer outro erro de performance.
Construir componentes conscientes de scroll em React requer entender quando evitar completamente o sistema de estado do React. Este artigo cobre as ferramentas certas para rastreamento de posição de scroll em React, por que Intersection Observer em React deve ser sua escolha padrão, e como implementar manipulação de scroll performática quando você genuinamente precisa de dados brutos de scroll.
Pontos-Chave
- Intersection Observer lida com a maioria dos padrões conscientes de scroll sem custo de performance
- Quando você precisa de valores contínuos de scroll, use refs e
requestAnimationFrameem vez de state - Reserve
useSyncExternalStorepara estado de scroll compartilhado entre componentes - Teste com roteamento de framework e sempre forneça alternativas de reduced-motion
Por Que Intersection Observer Deve Ser Sua Primeira Escolha
A maioria dos padrões de UI React orientados por scroll não precisam realmente da posição de scroll. Eles precisam de detecção de visibilidade: Este elemento está na tela? O usuário já passou por esta seção?
Intersection Observer lida com esses casos sem executar código da aplicação a cada evento de scroll. O navegador otimiza a observação internamente, tornando-a dramaticamente mais performática do que listeners de scroll.
import { useInView } from 'react-intersection-observer';
function FadeInSection({ children }) {
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: true,
});
return (
<div ref={ref} className={inView ? 'visible' : 'hidden'}>
{children}
</div>
);
}
A biblioteca react-intersection-observer encapsula esta API de forma limpa. Use-a para rastreamento de seções, lazy loading e animações de revelação.
Quando Você Realmente Precisa da Posição de Scroll
Alguns padrões requerem valores contínuos de scroll: efeitos parallax, indicadores de progresso ou transformações vinculadas ao scroll. É aqui que a maioria dos tutoriais falha com você.
Nunca faça isso:
// Isso causa re-renderizações a cada tick de scroll
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
Este padrão aciona o processo de reconciliação do React 60+ vezes por segundo durante o scroll ativo.
O Padrão Baseado em Ref
Armazene valores de scroll em refs e atualize o DOM diretamente:
import { useRef, useEffect } from 'react';
function ParallaxElement() {
const elementRef = useRef(null);
const rafId = useRef(null);
useEffect(() => {
const handleScroll = () => {
if (rafId.current) return;
rafId.current = requestAnimationFrame(() => {
if (elementRef.current) {
const offset = window.scrollY * 0.5;
elementRef.current.style.transform = `translateY(${offset}px)`;
}
rafId.current = null;
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
if (rafId.current) cancelAnimationFrame(rafId.current);
};
}, []);
return <div ref={elementRef}>Conteúdo parallax</div>;
}
Detalhes importantes: requestAnimationFrame limita as atualizações à taxa de atualização da tela. A opção { passive: true } informa ao navegador que você não chamará preventDefault(), habilitando otimizações de scroll.
Discover how at OpenReplay.com.
useSyncExternalStore para Estado de Scroll Compartilhado
Quando múltiplos componentes precisam da posição de scroll, trate-a como um store externo:
import { useSyncExternalStore } from 'react';
const scrollStore = {
subscribe: (callback) => {
window.addEventListener('scroll', callback, { passive: true });
return () => window.removeEventListener('scroll', callback);
},
getSnapshot: () => window.scrollY,
getServerSnapshot: () => 0,
};
function useScrollPosition() {
return useSyncExternalStore(
scrollStore.subscribe,
scrollStore.getSnapshot,
scrollStore.getServerSnapshot
);
}
Este padrão funciona corretamente com os recursos concorrentes do React e lida com renderização no lado do servidor. Na prática, você ainda deve limitar ou granularizar as atualizações (por exemplo, emitindo mudanças de seção em vez de pixels brutos) para evitar re-renderização a cada frame de scroll.
useLayoutEffect vs useEffect para Scroll
Use useLayoutEffect quando você precisa medir elementos DOM antes do navegador pintar. Isso previne flickering visual ao calcular posições com getBoundingClientRect().
Use useEffect para anexar listeners de scroll. A configuração do listener não precisa bloquear a pintura.
Considerações sobre Frameworks
Next.js App Router e frameworks similares gerenciam a restauração de scroll automaticamente. Manipuladores de scroll customizados podem conflitar com esse comportamento. Teste componentes conscientes de scroll após navegação do lado do cliente para detectar problemas de restauração.
Animações CSS Orientadas por Scroll
Navegadores modernos suportam animation-timeline: scroll() para efeitos de scroll puramente em CSS. O suporte dos navegadores permanece incompleto, mas esta abordagem elimina JavaScript completamente para casos suportados. Use-a com progressive enhancement—forneça uma experiência de fallback para navegadores não suportados.
Acessibilidade
Sempre respeite prefers-reduced-motion. Envolva animações acionadas por scroll:
const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
Pule animações ou forneça transições instantâneas quando isso retornar true.
Conclusão
Componentes conscientes de scroll não precisam destruir a performance da sua aplicação React. A chave é escolher a ferramenta certa para cada caso de uso. Alcance Intersection Observer primeiro—ele lida com detecção de visibilidade sem sobrecarga de performance. Quando você genuinamente precisa de valores contínuos de scroll, contorne o sistema de estado do React usando refs e requestAnimationFrame para manipulação direta do DOM. Para estado de scroll compartilhado entre múltiplos componentes, useSyncExternalStore fornece um padrão limpo e seguro para concorrência. Qualquer que seja a abordagem escolhida, teste minuciosamente com o roteamento do seu framework e sempre respeite as preferências do usuário para movimento reduzido.
Perguntas Frequentes
Definir state a cada evento de scroll aciona o processo de reconciliação do React 60+ vezes por segundo. Cada atualização de state causa uma re-renderização, o que significa que o React deve fazer diff da virtual DOM e potencialmente atualizar o DOM real repetidamente. Isso sobrecarrega a thread principal e causa travamentos visíveis. Usar refs contorna isso completamente, já que atualizações de ref não acionam re-renderizações.
Use useSyncExternalStore quando múltiplos componentes precisam do mesmo valor de posição de scroll. Ele se integra adequadamente com os recursos de renderização concorrente do React e lida com casos extremos como tearing, onde diferentes componentes podem ver valores diferentes durante uma renderização. Ele também fornece suporte integrado para renderização no lado do servidor através de getServerSnapshot.
Você pode usá-las com progressive enhancement. Animações CSS orientadas por scroll atualmente funcionam no Chrome e Edge, com suporte no Safari e Firefox ainda incompleto ou em progresso. Implemente-as como uma melhoria para navegadores suportados enquanto fornece um fallback em JavaScript ou experiência simplificada para outros. Verifique caniuse.com para suporte atual dos navegadores antes de depender delas.
Teste após navegação do lado do cliente, não apenas no carregamento inicial da página. O App Router gerencia a restauração de scroll automaticamente, o que pode conflitar com manipuladores de scroll customizados. Navegue entre páginas usando componentes Link e verifique se seus listeners de scroll se reconectam corretamente e não interferem com o comportamento de restauração de posição de scroll do framework.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.