Suavizando transições em listas React com startTransition, useDeferredValue e Suspense
Por John Pires · 13 de março de 2026

Suavizando transições em listas React com startTransition, useDeferredValue e Suspense
1) Quando e por que usar
Problema observável em listas grandes
Usuários digitam em um filtro de pesquisa e a UI trava enquanto o React recalcula uma lista grande. O input fica pouco responsivo, a interação parece lenta, e a experiência degrada especialmente em dispositivos de baixa CPU. Esse é um problema clássico: atualizações de estado síncronas e caros re-renders bloqueiam a thread principal.
Objetivo das APIs de transição
APIs como startTransition, useDeferredValue e Suspense existem para separar atualizações que precisam ser imediatas (responsividade do input) das que podem ser adiadas (renderização da lista). A ideia é manter o input fluido e apresentar um fallback ou uma versão gradual da lista enquanto o trabalho pesado conclui.
Use startTransition quando você tem atualizações que podem ser demarcadas como de baixa prioridade. Use useDeferredValue quando quer derivar um valor que pode ser adiado automaticamente. Use Suspense para organizar placeholders enquanto recursos assíncronos (código ou dados) ainda não estão prontos.
2) Comparação com alternativas
Debounce e throttling
Debounce reduz a frequência de atualização, evitando cálculos por cada tecla, mas introduz latência no feedback. Throttle limita chamadas por segundo, ainda pode deixar o input com sensação de atraso. Ambos são úteis, mas resolvem sintomas — não priorizam renderizações no scheduler do React.
Web Workers e off-main-thread
Extrair filtragem para um Web Worker resolve processamento pesado, mantendo a thread principal limpa. Porém, tem custo de serialização/deserialização e complexidade. Além disso, sincronizar estados e atualizações via postMessage adiciona latência e complexidade arquitetural.
Por que preferir startTransition/useDeferredValue
Essas APIs funcionam com o Scheduler do React: você continua no mesmo thread e usa o mecanismo interno para priorização. Em muitos casos isso é mais simples e integrado ao fluxo de componentes do que mover lógica para workers ou aplicar debounce indiscriminado.
3) Implementação progressiva
Exemplo 1: startTransition para priorizar o input
O exemplo abaixo mostra um input de busca que usa startTransition para colocar a atualização da lista em baixa prioridade. A entrada do usuário é atualizada imediatamente, enquanto o cálculo da lista é marcado como transição.
Contexto: componente que mantém o 'query' e 'filtered', atualiza 'filtered' dentro de startTransition para evitar bloquear o evento do input.
import React, {useState, startTransition, useMemo} from 'react'
function generateItems(n) {
return Array.from({length: n}, (_, i) => ({id: i, name: 'Item ' + i}))
}
export default function LargeListStartTransition() {
const items = useMemo(() => generateItems(50000), [])
const [query, setQuery] = useState('')
const [filtered, setFiltered] = useState(items)
function onChange(e) {
const next = e.target.value
setQuery(next) // resposta imediata do input
startTransition(() => {
// trabalho de baixa prioridade
setFiltered(items.filter(it => it.name.toLowerCase().includes(next.toLowerCase())))
})
}
return (
{filtered.slice(0, 200).map(it => (
{it.name}
))}
)
}O que acontece: o input fica responsivo; a lista atualiza assim que o React schedule completar a transição. Em janelas gráficas pesadas, você verá menos travamentos.
Exemplo 2: useDeferredValue para derivar um valor adiado
useDeferredValue cria uma versão adiada de um valor. No caso de busca, é ideal para derivar o texto buscado que alimenta filtros caros sem tocar o valor imediato do input.
Contexto: input atualiza 'query' imediatamente; a lista usa 'deferredQuery' que muda com baixa prioridade.
import React, {useState, useDeferredValue, useMemo} from 'react'
function generateItems(n) {
return Array.from({length: n}, (_, i) => ({id: i, name: 'Product ' + i}))
}
export default function LargeListDeferredValue() {
const items = useMemo(() => generateItems(30000), [])
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const filtered = useMemo(() =>
items.filter(it => it.name.toLowerCase().includes(deferredQuery.toLowerCase())),
[items, deferredQuery]
)
return (
Pesquisando por: {deferredQuery}
{filtered.slice(0, 200).map(it => (
{it.name}
))}
)
}O que acontece: enquanto a derivação do estado adiado não é atualizada, a UI pode renderizar uma versão anterior da lista, evitando re-renders constantes durante digitação.
Erros comuns e armadilhas
É comum usar startTransition sem entender que ainda haverá trabalho; não é uma bala mágica. Se o trabalho de render for realmente pesado, pode ser preciso reestruturar componentes ou usar pagination/virtualization. Outra armadilha é usar useDeferredValue para valores que precisam ser consistentes imediatamente, criando spikes de UI inconsistente.
Dica: combine estas APIs com list virtualization (react-virtual, tanstack/virtual) para o melhor resultado em listas muito grandes.
4) Integração com ecossistema real
Virtualização e priorização
Virtualização reduz elementos DOM e é frequentemente a primeira otimização. Use startTransition/useDeferredValue para priorizar atualizações, e virtualize a renderização para reduzir custo por frame. Juntos, eles tratam tanto a priorização quanto o custo absoluto de render.
Suspense para carregamento por item
Ao carregar dados por item (por exemplo, detalhes de cada linha), Suspense facilita mostrar placeholders individuais enquanto cada item finaliza a fetch. A abordagem funciona bem com um cache que 'suspende' usando o padrão createResource.
Contexto: abaixo um pequeno recurso que simula fetch e suspende. Cada ItemRow usa Suspense para mostrar fallback enquanto os dados chegam.
import React, {Suspense, useMemo} from 'react'
// createResource simples que suspende
function createResource(promiseFactory) {
let status = 'pending'
let result
const suspender = promiseFactory().then(
r => { status = 'success'; result = r },
e => { status = 'error'; result = e }
)
return {
read() {
if (status === 'pending') throw suspender
if (status === 'error') throw result
return result
}
}
}
function fetchItem(id) {
return new Promise(res => setTimeout(() => res({id, details: 'Detalhes do item ' + id}), 500 + (id % 5) * 200))
}
const cache = new Map()
function getItemResource(id) {
if (!cache.has(id)) cache.set(id, createResource(() => fetchItem(id)))
return cache.get(id)
}
function ItemRow({id}) {
const resource = getItemResource(id)
const data = resource.read()
return #{data.id} - {data.details}
}
export function ListWithSuspense({ids}) {
return (
{ids.map(id => (
carregando item {id}...}>
))}
)
}O que acontece: cada row mostra um fallback até que seus dados estejam prontos. Isso melhora percepção de velocidade, pois partes prontas aparecem primeiro.
Combinação prática: input responsivo + Suspense por linha
Combine startTransition/useDeferredValue para o filtro com Suspense em cada linha para dados adicionais. Resultado: input rápido, lista atualiza gradualmente, e detalhes de cada item aparecem conforme chegam.
5) Checklist de qualidade para produção
Critérios de aceitação de performance
Antes de liberar, valide os seguintes pontos: tempo de resposta do input abaixo de 100ms, menos de 15 frames perdidos em uma sequência de digitação rápida, memória estável com lista virtualizada e ausência de vazamentos no cache de Suspense.
Testes, medição e limites
Perf: use ferramentas como Chrome DevTools e React Profiler para identificar commits caros.
Testes de integração: scripts que simulam digitação rápida para checar regressões.
Métricas reais: RUM para medir tempo de input e percorrimento de lista no ambiente do usuário.
Inclua também limites de fallback: se a filtragem ainda for demasiadamente custosa, considere limites de tamanho de lista, paginação ou pesquisa no servidor.
Erros operacionais comuns
Confiar apenas em startTransition sem reduzir o custo de renderização por item.
Manter caches de Suspense indefinidamente, levando a memory leaks.
Usar useDeferredValue para fluxo que precisa de consistência imediata, causando flashes de estado.
Conclusão prática
Resumo das recomendações
Para listas: comece virtualizando. Adicione startTransition para separar atualizações que podem ser adiada e useDeferredValue quando houver valores derivados caros. Use Suspense para dados por item e combine com placeholders para UX melhorada. Essas técnicas têm papéis complementares, não são mutuamente exclusivas.
Passo a passo recomendado para adotar em produção
1) Medir o problema. 2) Virtualizar lista. 3) Marcar transições com startTransition ou usar useDeferredValue para consultas. 4) Aplicar Suspense para carregamento granular. 5) Validar com perf tests e RUM.
Aplicando essas práticas você reduz travamentos, melhora a percepção de velocidade e mantém a arquitetura previsível. São mudanças incrementais e auditáveis, que podem ser revertidas facilmente se impactarem outros fluxos.
Próximo passo prático: implemente um POC com os exemplos acima, ligue o profiler e compare antes/depois. Se houver ganho significativo, evolua para cache com limites e limpeza para produção.
Conclusão: priorização explícita (startTransition), valores adiados (useDeferredValue) e placeholders assíncronos (Suspense) são ferramentas complementares que, usadas em conjunto com virtualização e medições, trazem uma melhoria concreta de UX em listas React.
Assuntos: React, hooks, suspense, Performance, starttransition, usedeferredvalue
Leia também
useEffect vs useLayoutEffect no React: quando usar, exemplos e armadilhas
Compare useEffect e useLayoutEffect com exemplos práticos: quando usar cada um, medição de DOM sem layout shift, armadilhas comuns e padrões seguros para…
Context selectors: reduzir re-renders e otimizar estado global no React
Context selectors reduzem re-renders no React ao permitir que componentes leiam apenas fatias do estado global. Este artigo mostra quando usar, compara…
Corrigindo mismatches de hydration no React App Router (SSR): passo a passo
Guia prático e direto para identificar e corrigir mismatches de hydration no React App Router (SSR). Passo a passo: ambiente, exemplos, mini-projeto com…
Criando useStableCallback (useEvent): callbacks estáveis no React
Aprenda a criar useStableCallback, um hook que fornece callbacks com identidade estável mas que chamam a versão mais recente do handler. Mostro o erro comum…
Melhorar LCP com React Server Components e Suspense: streaming, placeholders e revalidação
Reduza LCP com React Server Components e Suspense: prioridades de dados, streaming incremental, placeholders que preservam layout e revalidação de cache…
Sincronizar estado entre Server e Client Components com use e useSyncExternalStore
Padronize estado entre Server e Client Components usando use (servidor) e useSyncExternalStore (cliente). Evite fetch duplicado, hydrate sem mismatch e…