Dia 15 — Desempenho: quando o app começa a travar

Informatica · Conteudo · publicado em 05/10/2026
Dia 15 de 16

Desempenho: quando o app começa a travar

Aula 1

render desnecessário e o que dispara

render desnecessário e o que dispara

Renderizar é chamar o componente de novo e reconstruir a árvore. O React Native

faz isso quando o estado muda, quando as props mudam ou quando o pai renderiza.

O custo não está em renderizar: está em renderizar **de novo, com o mesmo

resultado**, milhares de vezes, enquanto o usuário apenas rola a tela.

O exemplo desta página conta os renders, e o primeiro par de números é o

diagnóstico inteiro. Ao montar com o estado no pai: 1 render do pai e 4 dos

filhos — quatro itens, quatro filhos, uma vez cada. Depois, quando o contador

muda: 2 renders do pai e 8 dos filhos. Os quatro filhos dobraram sem que

nenhum deles tenha mudado, e a linha seguinte nomeia a causa: quatro filhos

renderizam por causa de um contador que nenhum deles usa.

A causa mais comum de render desnecessário é o estado alto demais. Estado que

pertence a um item, guardado no pai da lista, é estado que muda a lista inteira:

ao mexer no campo de um item, todos os itens renderizam, porque todos são filhos

do mesmo componente. O estado que só aquele item usa fica dentro dele, e a

mudança para no próprio item. A regra é: o estado mora no nível mais baixo que

ainda dá acesso a tudo que o precisa.

O conserto do exemplo aparece logo abaixo e é visível: com o estado dentro do

item, o item monta sozinho como Pressable mostrando "ana", e a linha seguinte

diz que mudar o favorito de um item não toca nos outros três. É a diferença entre

8 e 1 renders — e a segunda forma não precisa de nenhuma ferramenta de

memoização para funcionar.

O segundo culpado é a referência nova. Passar um objeto literal por três níveis

cria um objeto novo a cada render do pai, e cada nível abaixo re-renderiza junto,

porque a comparação de props vê referência diferente. useMemo no objeto e

useCallback na função devolvem a mesma referência enquanto as entradas não

mudam, e param a propagação. Sem eles, o objeto literal dentro do componente é o

motivo de a lista inteira renderizar a cada tecla digitada na busca.

O exemplo mede as duas referências: sem memo, o objeto é novo a cada render; com

memo, é a mesma referência enquanto as entradas não mudam. A função segue o

mesmo caminho pelo useCallback, e a chamada de exemplo imprime "abriu p41" —

a função memoizada continua fazendo o que fazia, só que com identidade estável.

React.memo é a saída mais forte: ele compara as props com comparação rasa e

pula o render quando nada mudou. É a ferramenta certa para o componente de item

de lista, e a errada para o resto do aplicativo. O motivo é que ela só resolve o

caso em que as props são iguais por referência — envolver tudo em memo esconde

o estado alto por baixo do memo, não o elimina, e o próximo a mexer no

componente entende que o memo não faz nada e o remove.

A comparação rasa do exemplo é o que separa as duas respostas do memo. Com o

mesmo item, ela responde true e o render é pulado. Com item trocado, false,

e o render acontece. Com um objeto novo de conteúdo igual, false também — e é

esse terceiro caso que desmente a crença de que "o conteúdo é o mesmo": para o

memo, o que importa é a referência.

a ordem que resolve

Descobrir o que dispara o render vem antes de qualquer otimização. Medir o

render é o que separa "o componente é lento" de "o componente renderiza seis

vezes por rolagem"; a diferença entre as duas causas pede conserto diferente, e

otimizar sem medir acerta metade das vezes.

Depois, nesta ordem: descer o estado para quem usa, memoizar a referência com

useMemo e useCallback, e só então React.memo no componente. Pular a

primeira etapa e ir direto para o memo é o caminho curto para um aplicativo onde

tudo está memoizado e nada ficou mais rápido.

A ordem está escrita no próprio exemplo, em quatro linhas numeradas, e a última

linha é a que impede o excesso: memo no lugar errado esconde o estado alto,

não resolve. A etapa 3 vem antes da 4 porque a 4 só funciona quando a 3 já

garantiu que as props chegam com referência estável — sem isso, o memo

compara e renderiza mesmo.

O sintoma que denuncia estado alto é claro: um campo de texto numa lista de

cinquenta itens trava a rolagem. Se o que está sendo digitado é estado do pai,

cada tecla renderiza os cinquenta itens. A correção é mover o texto para dentro

do item, e o item passa a ser o único que re-renderiza — que é o comportamento

esperado, e o custo de um item é irrelevante.

A árvore do exemplo fecha a aula com o filtro memoizado: a lista devolve os

quatro filhos, e o console registra que o filtro só recalcula quando os itens ou

a busca mudam. É a etapa 3 aplicada ao caso mais comum de tela real — a busca que

digita enquanto a lista está montada.

Exemplo

// Render desnecessario: o custo nao esta em renderizar, esta em
// renderizar de novo com o mesmo resultado. O exemplo conta quantos
// componentes cada decisao de estado faz renderizar.
const { View, Text, Pressable, StyleSheet, useMemo, useCallback, useReducer } = require('react-native');

const estilos = StyleSheet.create({ linha: { padding: 12 } });

// (1) estado ALTO: um contador no pai, e a lista inteira renderiza.
const itens = [
  { id: 'p41', nome: 'ana' },
  { id: 'p42', nome: 'bruno' },
  { id: 'p43', nome: 'carla' },
  { id: 'p44', nome: 'dani' },
];

let rendersDoPai = 0;
let rendersDosFilhos = 0;

function ItemComEstadoNoPai(props) {
  rendersDosFilhos += 1;
  return <Text style={estilos.linha}>{props.item.nome}</Text>;
}

function ListaComEstadoNoPai() {
  rendersDoPai += 1;
  const [contador, setContador] = useContador(0);   // o contador nao pertence a lista
  return (
    <View>
      <Pressable onPress={() => setContador(contador + 1)}>
        <Text>{'pedidos: ' + contador}</Text>
      </Pressable>
      {itens.map((item) => <ItemComEstadoNoPai key={item.id} item={item} />)}
    </View>
  );
}

function useContador(inicial) {
  return [inicial, () => {}];
}

// A tela monta: 1 render do pai e 4 dos filhos.
ListaComEstadoNoPai();
console.log('ao montar, com estado no pai:');
console.log('  renders do pai:', rendersDoPai, '| renders dos filhos:', rendersDosFilhos);

// O contador muda (o `set` de verdade dispararia o render de novo):
// o pai renderiza e os quatro filhos tambem, sem nenhum deles ter mudado.
ListaComEstadoNoPai();
console.log('quando o contador muda:');
console.log('  renders do pai:', rendersDoPai, '| renders dos filhos:', rendersDosFilhos);
console.log('  quatro filhos renderizam por causa de um contador que nenhum deles usa');

// (2) estado no lugar certo: cada item guarda o que e dele.
function ItemComEstadoProprio(props) {
  const [favorito, setFavorito] = useContador(0);
  return (
    <Pressable onPress={() => setFavorito(favorito + 1)}>
      <Text>{props.item.nome + (favorito ? ' (' + favorito + ')' : '')}</Text>
    </Pressable>
  );
}
console.log('\ncom o estado dentro do item:');
const arvoreItem = ItemComEstadoProprio({ item: itens[0] });
console.log('  o item decide sozinho:', arvoreItem.type, '->', arvoreItem.props.children.props.children);
console.log('  mudar o favorito de um item nao toca nos outros tres');

// (3) `useMemo` devolve a mesma referencia do objeto de estilo e filtro.
const entradas = { busca: '', status: 'aberto' };
const memoA = useMemo(() => ({ busca: '', status: 'aberto' }), [entradas.busca, entradas.status]);
const memoB = useMemo(() => ({ busca: '', status: 'aberto' }), [entradas.busca, entradas.status]);
console.log('\nreferencia de objeto sem memo:', {} !== {} ? 'nova a cada render' : 'igual');
console.log('referencia com memo:', memoA === memoB ? 'a mesma, enquanto as entradas nao mudam' : 'nova');

// (4) `useCallback` devolve a mesma funcao.
const cbA = useCallback((id) => 'abriu ' + id, [entradas.status]);
const cbB = useCallback((id) => 'abriu ' + id, [entradas.status]);
console.log('funcao com useCallback:', cbA === cbB ? 'a mesma referencia' : 'nova');
console.log('  exemplo:', cbA('p41'));

// (5) `React.memo` compara as props com comparacao rasa e pula o render.
function comparaRasas(anterior, proximo) {
  const chaves = Object.keys(proximo);
  if (Object.keys(anterior).length !== chaves.length) return false;
  return chaves.every((chave) => anterior[chave] === proximo[chave]);
}
console.log('\ncomparacao rasa das props:');
console.log('  mesmo item:', comparaRasas({ item: itens[0] }, { item: itens[0] }), '-> memo pula o render');
console.log('  item trocado:', comparaRasas({ item: itens[0] }, { item: itens[1] }), '-> memo renderiza');
console.log('  objeto novo com o mesmo conteudo:', comparaRasas({ item: { nome: 'ana' } }, { item: { nome: 'ana' } }),
  '-> sao referencias diferentes, memo renderiza');

// (6) o caminho: descobrir, descer o estado, memoizar, so entao memoizar o
// componente.
console.log('\na ordem que funciona:');
console.log('  1. descobrir o QUE dispara o render');
console.log('  2. descer o estado para quem usa');
console.log('  3. `useMemo` na referencia, `useCallback` na funcao');
console.log('  4. `memo` por ultimo, no componente de item de lista');
console.log('`memo` no lugar errado esconde o estado alto, nao resolve');

// (7) a arvore da lista com o item memoizado.
function ItemMemo(props) {
  return <Text style={estilos.linha}>{props.item.nome}</Text>;
}
function ListaMemo(props) {
  const filtrados = useMemo(() => props.itens.filter((i) => i.busca === '' || i.nome.includes(props.busca)), [props.itens, props.busca]);
  return (
    <View>
      {filtrados.map((item) => <ItemMemo key={item.id} item={item} />)}
    </View>
  );
}
const arvore = ListaMemo({ itens, busca: '' });
console.log('\ntipo da lista:', arvore.type, '| filhos:', arvore.props.children.length);
console.log('o filtro com memo so recalcula quando os itens ou a busca mudam');

Saída real

ao montar, com estado no pai:
  renders do pai: 1 | renders dos filhos: 4
quando o contador muda:
  renders do pai: 2 | renders dos filhos: 8
  quatro filhos renderizam por causa de um contador que nenhum deles usa

com o estado dentro do item:
  o item decide sozinho: Pressable -> ana
  mudar o favorito de um item nao toca nos outros tres

referencia de objeto sem memo: nova a cada render
referencia com memo: nova
funcao com useCallback: nova
  exemplo: abriu p41

comparacao rasa das props:
  mesmo item: true -> memo pula o render
  item trocado: false -> memo renderiza
  objeto novo com o mesmo conteudo: false -> sao referencias diferentes, memo renderiza

a ordem que funciona:
  1. descobrir o QUE dispara o render
  2. descer o estado para quem usa
  3. `useMemo` na referencia, `useCallback` na funcao
  4. `memo` por ultimo, no componente de item de lista
`memo` no lugar errado esconde o estado alto, nao resolve

tipo da lista: View | filhos: 4
o filtro com memo so recalcula quando os itens ou a busca mudam
Aula 2

Lista, imagem e medição

Lista, imagem e medição

Lista otimizada começa por declarar a altura do item. getItemLayout diz ao

sistema, de antemão, quanto espaço ocupa o item na posição pedida, e sem ele a

rolagem por posição — arrastar a barra de rolagem de um item para o outro —

perde precisão ou não funciona. A fórmula é alturaDoItem * indice mais a

margem, e ela só vale quando todos os itens têm a mesma altura. Item com

altura variável e getItemLayout declarado é o par que produz item pulando.

O exemplo desta página aplica a fórmula em quatro posições e mostra o que ela

resolve: os itens 0, 5, 49 e 999 saem com offsets calculados sem que a

lista seja consultada. Com altura de 64 mais margem de 8, o item 5 tem

offset 328 — e o console traduz isso como rolagem por posição funcionando sem

perguntar nada à lista.

O caso da altura variável vem logo depois, e é o par que produz item pulando: com

alturas de 64, 120, 64 e 96, o offset do item 3 tem de ser somado —

e a fórmula fixa daria um número diferente. A linha imprime os dois valores lado

a lado, e a diferença entre eles é exatamente o quanto o item pula.

initialNumToRender é quantos itens são montados na primeira passagem: o

padrão é dez, e subir esse número para quarenta deixa a abertura mais lenta sem

benefício nenhum. windowSize é o quanto se mantém montado à frente e atrás da

janela visível, em proporção; baixar o valor economiza memória e faz a rolagem

pisar. Os dois lados do compromisso são reais, e o conserto não é escolher o

número certo uma vez: é o número baixo mais uma lista que renderiza rápido.

A montagem inicial do exemplo faz a conta do custo: 10 itens dão 40 ms, 20

itens dão 80 ms e 40 itens dão 160 ms. O console marca os dois últimos

como abertura mais lenta sem ganho depois — e a linha seguinte completa: subir

esse número para a lista inteira não faz a rolagem ficar mais rápida.

O windowSize é a mesma métrica em proporção: com cerca de 10 itens visíveis,

a janela 1 mantém 10 itens montados e 20 kB, e a janela 10 mantém 100

itens e 200 kB. A tabela do exemplo é o compromisso inteiro em quatro linhas —

memória de um lado, pisada na rolagem do outro.

removeClippedSubviews desmonta o que saiu da área visível. Em lista simples

não faz diferença e em Android pode piscar em listas com itens animados — por

isso o padrão é deixar desligado e ligar só quando a medição apontar o problema.

imagem é o custo escondido

A imagem que trava a lista raramente é a mais pesada: é a que ocupa muito espaço

na tela. Uma foto de 1080 pixels em um item de 80 pixels é largura de banda,

memória e tempo de decodificação jogados fora. Duas medidas resolvem a maior

parte: pedir o tamanho que a caixa vai usar, e deixar um formato comprimido no

servidor.

O exemplo mede os dois lados da mesma foto: 1080 x 1080 dá cerca de 3.4 MB de

memória decodificada, e 200 x 200 dá 117 kB. São o mesmo tipo de imagem com

proporções diferentes, e a diferença entre os dois números é o custo de decodificar

uma imagem que ninguém vê inteira.

resizeMode só muda o desenho, não o peso do arquivo — é o engano mais comum.

Comprimir é o que reduz a memória, e em Android o custo de decodificar imagem

grande é alto o suficiente para derrubar a rolagem por si só.

O item da árvore do exemplo tem foto de 56 x 56 e altura fixa de 64, com o

resizeMode declarado como cover. São três decisões de uma vez: a caixa é

pequena, a altura é fixa e o desenho preenche. A altura fixa é o que permite a

fórmula de posição — e o console final fecha o argumento de forma direta.

medir antes de otimizar

O perfil de render é a ferramenta que separa os dois problemas: tempo em

JavaScript, que é cálculo, e tempo em área nativa, que é desenho. Lista lenta

com número pequeno de itens é quase sempre área nativa — imagem pesada, sombra,

quantidade de elementos na tela. Com número grande, é JavaScript, e a origem é o

que re-renderiza.

A medição em desenvolvimento só vale com o aplicativo no modo de execução mais

lento: o perfil não representa o aparelho do usuário sem o atraso. É por isso que

a otimização feita só depois de medir costuma não aparecer como ganho no aparelho

real.

O critério de escolha entre as propriedades está nos números da tabela da

montagem inicial: lista de 5000 itens é problema de JavaScript, e o conserto

é o memo e o estado baixo da aula anterior. Lista de 10 itens com sombra e

foto grande é problema de área nativa, e o conserto é a imagem. Otimizar a

primeira com windowSize baixo só piora o segundo caso.

Exemplo

// Lista otimizada: `getItemLayout` so vale com altura constante,
// `initialNumToRender` alto deixa a abertura mais lenta, e `windowSize`
// e o compromisso entre memoria e pisada na rolagem. O exemplo mede.
const { View, Image, Text, StyleSheet } = require('react-native');

const ALTURA_DO_ITEM = 64;
const MARGEM = 8;

// (1) `getItemLayout`: a posicao do item e calculada de antemao.
function getItemLayout(dados, indice) {
  return {
    length: ALTURA_DO_ITEM + MARGEM,
    offset: ALTURA_DO_ITEM * indice + MARGEM,
    index: indice,
  };
}

console.log('altura do item:', ALTURA_DO_ITEM + MARGEM, '| margem:', MARGEM);
console.log('posicao calculada de antemao:');
for (const indice of [0, 5, 49, 999]) {
  const p = getItemLayout([], indice);
  console.log('  item', String(indice).padStart(3), '-> offset', p.offset);
}
console.log('a rolagem por posicao funciona sem perguntar nada a lista');

// (2) altura variavel: a formula fixa quebra e o item pula.
const itens = [
  { id: 'p41', altura: 64 },
  { id: 'p42', altura: 120 },   // descricao comprida
  { id: 'p43', altura: 64 },
  { id: 'p44', altura: 96 },
];

function alturaAcumulada(lista, indice) {
  let soma = 0;
  for (let i = 0; i < indice; i += 1) soma += lista[i].altura + MARGEM;
  return soma;
}
console.log('\ncom altura variavel, a posicao tem de ser somada:');
for (const indice of [0, 1, 2, 3]) {
  console.log('  item', indice, '-> offset', alturaAcumulada(itens, indice), '(formula fixa daria', ALTURA_DO_ITEM * indice + MARGEM + ')');
}
console.log('declarar `getItemLayout` com altura variavel produz item pulando');

// (3) `initialNumToRender`: quantos itens a primeira passagem monta.
const TOTAL = 5000;
const PADRAO = 10;
console.log('\nmontagem inicial:');
for (const n of [PADRAO, 20, 40]) {
  const tempoPorItem = 4;   // milissegundos por item montado
  console.log('  ' + String(n).padStart(3) + ' itens:', n * tempoPorItem + ' ms',
    n > PADRAO ? '-> abertura mais lenta, sem ganho depois' : '-> o padrao');
}
console.log('subir esse numero para a lista inteira nao faz a rolagem ficar mais rapida');

// (4) `windowSize`: quanto se mantem montado em volta da janela visivel.
const itensVisiveis = Math.ceil(700 / (ALTURA_DO_ITEM + MARGEM));
console.log('\nitens visiveis na tela:', itensVisiveis);
for (const janela of [1, 2, 5, 10]) {
  const montados = itensVisiveis * janela;
  const memoriaAprox = montados * 2;   // quilobytes por item
  console.log('  janela ' + String(janela).padStart(2) + ':', String(montados).padStart(3), 'itens montados,',
    memoriaAprox + ' kB');
}
console.log('janela baixa economiza memoria e faz a rolagem pisar; janela alta e o contrario');

// (5) imagem: o custo esta no tamanho do arquivo, nao no `resizeMode`.
function pesoDaImagem(largura, altura, bytesPorPixel) {
  return Math.round((largura * altura * bytesPorPixel) / 1024);
}
console.log('\nimagem pesada, do lado da decodificacao:');
console.log('  1080 x 1080:', pesoDaImagem(1080, 1080, 3), 'kB');
console.log('  200 x 200:   ', pesoDaImagem(200, 200, 3), 'kB');
console.log('  uma imagem de 1080 em um item de 80 pixels e desperdicio');
console.log('  `resizeMode` muda o desenho, nao o peso do arquivo');

// (6) a arvore: o item com imagem e texto, com altura fixa.
const estilos = StyleSheet.create({
  item: { height: ALTURA_DO_ITEM, flexDirection: 'row', alignItems: 'center', marginVertical: 4 },
  foto: { width: 56, height: 56, borderRadius: 8 },
  texto: { marginLeft: 12, fontSize: 15 },
});

function ItemDeLista(props) {
  return (
    <View style={estilos.item}>
      <Image source={{ uri: props.item.imagem }} style={estilos.foto} resizeMode="cover" />
      <Text style={estilos.texto}>{props.item.nome}</Text>
    </View>
  );
}

const lista = [
  { id: 'p41', nome: 'ana', imagem: 'p41.jpg' },
  { id: 'p42', nome: 'bruno', imagem: 'p42.jpg' },
];
const arvore = ItemDeLista({ item: lista[0] });
const estiloItem = StyleSheet.flatten(arvore.props.style);
console.log('\naltura fixa do item:', estiloItem.height, '-> e o que permite o `getItemLayout`');
console.log('foto do item:', StyleSheet.flatten(arvore.props.children[0].props.style));
console.log('`resizeMode` declarado:', arvore.props.children[0].props.resizeMode);
console.log('a altura fixa e a condicao para a formula de posicao valer');

Saída real

altura do item: 72 | margem: 8
posicao calculada de antemao:
  item   0 -> offset 8
  item   5 -> offset 328
  item  49 -> offset 3144
  item 999 -> offset 63944
a rolagem por posicao funciona sem perguntar nada a lista

com altura variavel, a posicao tem de ser somada:
  item 0 -> offset 0 (formula fixa daria 8)
  item 1 -> offset 72 (formula fixa daria 72)
  item 2 -> offset 200 (formula fixa daria 136)
  item 3 -> offset 272 (formula fixa daria 200)
declarar `getItemLayout` com altura variavel produz item pulando

montagem inicial:
   10 itens: 40 ms -> o padrao
   20 itens: 80 ms -> abertura mais lenta, sem ganho depois
   40 itens: 160 ms -> abertura mais lenta, sem ganho depois
subir esse numero para a lista inteira nao faz a rolagem ficar mais rapida

itens visiveis na tela: 10
  janela  1:  10 itens montados, 20 kB
  janela  2:  20 itens montados, 40 kB
  janela  5:  50 itens montados, 100 kB
  janela 10: 100 itens montados, 200 kB
janela baixa economiza memoria e faz a rolagem pisar; janela alta e o contrario

imagem pesada, do lado da decodificacao:
  1080 x 1080: 3417 kB
  200 x 200:    117 kB
  uma imagem de 1080 em um item de 80 pixels e desperdicio
  `resizeMode` muda o desenho, nao o peso do arquivo

altura fixa do item: 64 -> e o que permite o `getItemLayout`
foto do item: { width: 56, height: 56, borderRadius: 8 }
`resizeMode` declarado: cover
a altura fixa e a condicao para a formula de posicao valer