Destino Dev

Blog

Evitar re-renders no Vue 3: shallowRef, markRaw e storeToRefs (Pinia)

Por John Pires · 11 de março de 2026

Evitar re-renders no Vue 3: shallowRef, markRaw e storeToRefs (Pinia)

Contexto e motivacao

O problema: re-renders desnecessários

Re-renders desnecessários são uma das causas mais comuns de problemas de performance em aplicações Vue 3 que crescem em complexidade. Componentes que dependem de objetos grandes, mapas ou classes frequentemente são forçados a renderizar quando qualquer parte do objeto reativo muda. Isso gera frames perdidos, processamento desnecessário e experiências de usuário menos responsivas.

Em aplicações com listas grandes, canvas/WebGL, ou integrações com bibliotecas externas (Three.js, Mapbox, WebSocket handlers), o custo de um re-render global pode ser alto. O objetivo aqui é reduzir o escopo das reações sem sacrificar previsibilidade nem introduzir bugs sutis.

Vamos analisar ferramentas específicas do Vue 3 e do ecossistema Pinia que ajudam a controlar quando e o que deve re-renderizar: shallowRef, markRaw e storeToRefs. Entender o comportamento interno do sistema reativo permite tomar decisões fundamentadas.

Impacto em performance e UX

Nem todo re-render é ruim — atualizações necessárias são essenciais. O problema é quando um componente é invalidado por uma mudança que não afeta seu output visual. O custo pode ser CPU, layout, e pintura repetida, além de efeitos colaterais em watchers, computed e lifecycles que reexecutam desnecessariamente.

Medir (devtools, Performance tab do browser) é obrigatório antes de otimizar prematuramente. Este artigo mostra patterns seguros para reduzir re-renders quando você comprovou que eles são um gargalo.

Como funciona por baixo dos panos

Sistema reativo do Vue 3 (Proxies)

Vue 3 usa Proxies para interceptar acessos e mutações de objetos reativos (reactive() e ref()). Quando uma propriedade é lida durante a execução de um efeito (computed/watchEffect), aquele efeito é registrado como dependente. Quando a propriedade é escrita, Vue notifica esses efeitos para reexecutar.

Isso significa que a granularidade da reatividade é por propriedade acessada (via get traps) e por mutação (via set traps). Para objetos aninhados, cada propriedade aninhada também vira dependência específica quando acessada — porém, envolver estruturas complexas em reactive() pode fazer com que operações de substituição inteira disparem invalidations maiores.

Change tracking e triggers

Existem dois vetores comuns que causam re-renders excessivos: substituir referências inteiras (ex: state.items = newArray) e tornar reativos objetos que não precisam ser observados profundamente (ex: instâncias de classes ou grandes coleções do DOM). Ambos disparam triggers que podem atingir muitos efeitos.

shallowRef cria uma referência reativa que não torna reativas as propriedades internas. markRaw remove um objeto do sistema reativo completamente. Entender a diferença é crucial: shallowRef ainda notifica quando a referência é trocada; markRaw não participa do sistema reativo.

Implementacao passo a passo

shallowRef: quando e como usar

shallowRef é útil quando você tem um container (array/objeto) cujo identity importa, mas as mutações internas não devem causar rastreamento profundo. Ex: uma lista grande onde você substitui o array todo com uma nova referência em operações de paginação, ou componentes que recebem um objeto imenso e você só precisa detectar quando a referência muda.

Exemplo prático: componente que recebe um array massivo e só precisa re-renderizar quando a referência do array muda (não quando mutações internas acontecem).

// Contexto: componente que armazena uma coleção grande e não quer tracking profundo
// O código abaixo usa shallowRef para evitar observar cada item da lista.
import { defineComponent, shallowRef, watchEffect } from 'vue'

export default defineComponent({
  props: { items: Array },
  setup(props) {
    const container = shallowRef(props.items)

    // O watchEffect reage apenas quando a referência 'container.value' muda
    watchEffect(() => {
      console.log('Referência de container alterada:', container.value?.length)
    })

    return { container }
  }
})

O que acontece: o watchEffect executa quando container.value é substituído. Alterações internas (push, splice se feitas diretamente no array original) não dispararão o watchEffect se você manipular o array sem trocar a referência.

markRaw: quando e como usar

markRaw é a escolha certa para objetos que não precisam de reatividade — por exemplo, instâncias de classes de bibliotecas externas (Three.js Scene), conexões WebSocket, caches na memória ou grandes Maps/Sets que você manipula imperativamente.

Exemplo prático: armazenar uma instância do cliente WebSocket no store sem causar re-renders dos componentes conectados ao store.

// Contexto: Pinia store armazenando um cliente WebSocket que NÃO deve ser reativo
import { defineStore } from 'pinia'
import { markRaw } from 'vue'

export const useSocketStore = defineStore('socket', {
  state: () => ({
    socket: null
  }),
  actions: {
    connect(url) {
      const ws = new WebSocket(url)
      this.socket = markRaw(ws) // evita reatividade pesada
    },
    send(data) {
      this.socket?.send(JSON.stringify(data))
    }
  }
})

O que acontece: a propriedade socket no estado do store contém a instância WebSocket, mas o Vue não criará proxies nem dependerá das suas propriedades internas. A substituição do valor (this.socket = markRaw(ws)) ainda é observada pelo store se você expuser a referência, mas operações internas do WebSocket não dispararão re-renders.

storeToRefs (Pinia): evitar reactivity-wide updates

Quando você usa um store Pinia dentro de um componente, acessar propriedades diretamente cria dependências. Desestruturar diretamente o store (const { items } = useStore()) cria refs que mantêm ligação reativa, mas pode levar à perda de reatividade ou causar re-renders se você não usar corretamente.

storeToRefs converte cada propriedade do store em refs independentes — isso reduz re-renderes ao permitir que componentes consumam apenas as props necessárias.

// Contexto: componente consumindo um store Pinia com várias propriedades
import { defineComponent } from 'vue'
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    const store = useMainStore()
    const { items, count } = storeToRefs(store)

    // 'items' e 'count' são refs individuais, mudanças em outras props do store
    // não forçam re-render aqui.
    return { items, count }
  }
})

O que acontece: o componente só fica reativo a items e count. Se o store atualizar outra propriedade (ex: filters), o componente que só usa items/count não será re-renderizado.

Patterns e boas praticas

Componentização e limites de reatividade

Fragmentar UI em componentes menores e responsáveis por fatias específicas do estado reduz blast radius de atualizações. Combine isso com técnicas abaixo para limitar reações apenas ao que importa.

  • Usar computed para mapear fatias do estado que realmente afetam o template.
  • Preferir props e emits quando a informação é local a um subtree, evitando leitura direta do store.
  • Extrair componentes puros (stateless) para que sejam renderizados apenas quando seus inputs mudam.

Exemplo: um componente de item de lista que recebe apenas os dados necessários como props evita depender da coleção inteira.

Normalização de dados e imutabilidade

Normalizar coleções (usar um objeto por id em vez de arrays aninhados) reduz mutações profundas e facilita updates por identidade. Trabalhe com cópias imutáveis quando for necessário trocar a referência e disparar updates controlados.

Exemplo: em vez de mutar arrays com push/splice no store, crie novos arrays com spread quando quiser notificar consumidores específicos. Use shallowRef quando você não quiser o rastreamento profundo mas ainda queira substituir a referência para sinalizar mudança.

Armadilhas comuns e como evitar

Misturar markRaw e mutações imperativas

markRaw remove a observabilidade — mas se você espera que a UI reflita mudanças internas daquele objeto, markRaw não é a escolha. É um atalho para performance que requer disciplina: se você marca uma instância como raw, então deve emitir manualmente sinais ou trocar referências para atualizar a UI.

Dica: combine markRaw com uma ref sinalizadora. Use uma pequena ref booleana ou um contador para indicar que algo mudou e a UI deve reagir.

Exemplo de padrão seguro combinando markRaw com um contador reativo que força uma atualização controlada.

// Contexto: usar um objeto não reativo, mas expor uma flag para o template
import { defineComponent, ref, markRaw } from 'vue'
import { useStore } from '@/stores/complex'

export default defineComponent({
  setup() {
    const store = useStore()
    store.client = markRaw(new SomeClient())
    const tick = ref(0)

    function updateView() {
      // operador imperativo que muda estado interno do client
      store.client.doSomething()
      // forçamos reatividade controlada
      tick.value++
    }

    return { tick, updateView }
  }
})

O que acontece: a UI que depende de tick será atualizada de forma controlada enquanto o client permanece raw.

Desestruturação incorreta de stores e perda de reatividade

Fazer const { items } = store pode extrair o valor atual, mas você perde a ligação reativa (passa a ter uma cópia). Isso causa bugs sutis: o componente não atualiza quando o store muda.

Use storeToRefs(store) ou acesse store.items diretamente no template/computed. Além disso, cuidado ao usar toRaw() — ele expõe o objeto não proxy e pode levar a leituras que ignoram dependências reativas.

Conclusão prática

Checklist para otimização

Antes de aplicar otimizações, siga este checklist mínimo:

  1. Meça com devtools/performance para confirmar o problema.
  2. Identifique o escopo do re-render: componente, árvore, ou store inteiro.
  3. Use storeToRefs para expor apenas as refs necessárias ao componente.
  4. Use shallowRef quando precisar controlar substituição de referência e evitar tracking profundo.
  5. Use markRaw para objetos que nunca devem participar do sistema reativo.
  6. Documente decisões: marque os objetos raw e explique a estratégia para futuros devs.

Implementar essas práticas reduz re-renders sem comprometer a previsibilidade do estado.

Próximos passos e medições

Depois de aplicar mudanças, re-meça. Procure reduzir o tempo de scripting e o número de reflows. Verifique também memory leaks: ao usar markRaw você pode criar objetos com ciclo de vida diferente; garanta que desconecte listeners e limpe recursos.

Este artigo focou em três ferramentas: shallowRef, markRaw e storeToRefs. Cada uma resolve um tipo diferente de problema. Em conjunto com componentização e imutabilidade, elas permitem controlar reatividade de forma fina e previsível.

Resumo final: não elimine a reatividade — modele-a. Prefira granularidade, não apagamento. Use shallowRef quando quer controlar profundidade, markRaw quando quer excluir do sistema reativo e storeToRefs para consumir stores sem re-render global.

Aplicando essas práticas, você mantém a API reativa do Vue 3, reduz custos de render e evita bugs causados por otimizações agressivas e mal documentadas.

Assuntos: Vue.js, pinia, reactivity, Performance, JavaScript

Leia também