Destino Dev

Blog

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

  1. Confiar apenas em startTransition sem reduzir o custo de renderização por item.

  2. Manter caches de Suspense indefinidamente, levando a memory leaks.

  3. 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