Como melhorar a performance no React Native

Lucas Pinheiro
10 min readSep 11, 2019

--

10 dicas práticas para melhorar o desempenho do seu aplicativo

Primeiramente, além de dizer que esse é meu primeiro artigo, gostaria de deixar claro que essas dicas não são específicas de código, mas também conceituais e de estruturação de uma aplicação, pois creio que a performance é uma questão de percepção. Ninguém ficara contando quantos milissegundos sua tela demora para carregar.

Essas dicas foram feitas para React Native, mas não significa que a maioria dos conceitos apresentados não podem ser usados para melhorar a performance de aplicações feitas usando React.js.

Essas dicas podem ser aplicadas em qualquer aplicativo de qualquer tamanho, porém, resolvi colocar dicas para redux e react-navigation, pois creio que são 2 bibliotecas muito utilizadas e que, geralmente, são a causa de lentidão nas aplicações.

1. Diminuir a quantidade de renderizações dos componentes

Uma das operações mais custosas, senão a mais, em termos de processamento, certamente é a renderização excessiva de um componente, mas, podemos facilmente identificar esse problema: o bom e velho console.log() no render() já é suficiente.

Identificado o problema, devemos achar o causador do problema, os mais comuns são: mudanças desnecessárias no state/props e a má estruturação do componente.

Daqui a pouco trataremos de mudanças nas props e no state que não precisariam renderizar novamente a aplicação.

O que eu quero dizer com má estruturação do componente? Bem, devemos sempre buscar diminuir a responsabilidade dos mesmos. Usaremos como exemplo uma tela de Login simples (baseada na pattern: Presentational e Container Components).

Teremos o LoginContainer, que será responsável por toda a lógica e somente por renderizar os componentes ‘burros’, que apenas recebem props e renderizam algo na tela.

LoginContainer.js

Teremos o LoginForm, um formulário que terá apenas 2 campos: email e senha, e um botão de enviar.

LoginForm.js

Aonde devemos colocar o state que guarda o email e a senha digitadas? No Container ou no Form? Poderíamos até deixar o state no Container, mas ele é específico do Formulário, e devemos diminuir as responsabilidades ao máximo.

Vamos ver as 2 situações quando o usuário digitar algo:

State no LoginContainer: todo o LoginContainer vai renderizar de novo, até componentes que não tem nada a ver com o formulário de login.

State no LoginForm: Apenas o formulário vai renderizar de novo.

Viram como é simples atribuir responsabilidades menores para componentes específicos? Ainda poderíamos ir mais longe, transformando cada input do formulário em um componente separado, assim quando o usuário digitar o email por exemplo, apenas o campo de email seria renderizado novamente, ao invés do formulário inteiro.

2. ShouldComponentUpdate e PureComponent

Muitas vezes nossa aplicação atualiza (devido a uma chamada na API pra atualizar os dados na tela, por exemplo) e acaba recebendo props iguais as anteriores, ou seja, se nada mudou, não precisamos renderizar novamente. Temos 2 soluções simples para esse problema:

Implementar o método (do ciclo de vida da aplicação) ShouldComponentUpdate ou utilizar PureComponent.

ShouldComponentUpdate é um método que recebe as próximas props e o próximo state e retorna um booleano (true ou false) para saber se devemos re-renderizar ou não.

Exemplo básico da implementação do método ShouldComponentUpdate

Ao implementarmos ele, podemos dizer que não queremos renderizar novamente se nada tiver mudado, ou se alguma prop específica não tiver mudado.

Muito resumidamente, um PureComponent já traz essa implementação para nós. Porém, não saia usando ele à toa, pense que verificar se algo mudou tem um custo de processamento.

Exemplo da utilização do PureComponent

Minha dica é: se você vê que seu componente costuma receber props que não mudaram, use-o, em outros casos, provavelmente não é necessário.

3. Não realizar operações pesadas no render()

Como dito anteriormente, renderizar é muito pesado, e realizar operações pesadas no render não ajuda em nada.

Não faça chamadas a API no render, para isso temos o ComponentDidMount e o useEffect(function, []) caso você esteja usando os React Hooks, que são métodos executados antes do componente renderizar.

componentDidMount
useEffect (React Hooks)

De preferência, trate seus dados (no back-end ou antes do render) antes de exibi-los, é muito comum ver a formatação da data (com a biblioteca moment, por exemplo) sendo feita dentro do render, o que causa um processamento extra desnecessário.

4. Usar Redux adequadamente

Assim como estruturamos nossos componentes com responsabilidades específicas, devemos estruturar nosso Redux da maneira correta.

Identifique qual State realmente precisa estar no Redux. Temos nosso formulário de Login que guarda email e senha, algum outro componente precisa dessa informação? Não, então ela não deve estar no Redux, geralmente nenhum State de formulário ficará no Redux.

Cuide e planeje suas Actions com cuidado, pois, cada Action disparada tem um custo de processamento e em excesso podem prejudicar sua aplicação.

É muito comum vermos Actions sendo disparadas para controlar o loading ao buscar dados numa API, por exemplo. Mas porque guardar o loading no redux? Ele será usado por outros componentes na aplicação?

Usaremos como exemplo um componente que busca uma lista de livros numa API, e ao buscar, queremos mostrar um ActivityIndicator para o usuário saber que algo está acontecendo (por baixo dos panos).

ActivityIndicators

Muitas vezes, não faz nenhum sentido guardar o loading no State do Redux, pois ele só será usado pelo nosso componente que busca os livros.

Exemplo de componente que mostra um ‘loading’ antes de listar os dados.

Portanto, avalie com cuidado a estrutura das suas Actions e dos seus reducers, nem tudo precisa estar no redux. No exemplo acima, poderíamos ter o método que busca livros e os próprios livros no redux, caso outros componentes precisassem dessas informações.

Minha dica é: procure sempre avaliar se algum State deve estar no redux, quando você começar a fazer ‘prop-drilling’ ou começar a ter dificuldades de compartilhar State entre componentes, chegou a hora de considerar o uso do redux.

5. Não usar loggers em produção

Não faça console.logs e nem utilize de bibliotecas de log como redux-logger em produção. Essas operações são custosas e desnecessárias, nenhum usuário verá seus logs.

Dê uma pesquisada sobre variáveis de ambiente (.env) para diferenciar seu ambiente de desenvolvimento (que poderá ter logs) do ambiente de produção.

Também pesquise sobre o plugin que remove os logs automaticamente: babel-plugin-transform-remove-console. Poderá ser útil.

6. Usar React Navigation corretamente

Estarei falando especificamente da versão 3 do React navigation, porém a versão 4 já está disponível e traz otimizações como o uso de componentes nativos, sugiro dar uma olhada.

Devemos estruturar corretamente nossa navegação, tanto para performance, quanto para melhorar a experiência do usuário. Performance não é sobre velocidade, é sobre percepção.

Vou citar um aplicativo muito famoso e 2 conceitos simples que ele usa: o Instagram. Já pararam pra pensar porque o Instagram usa uma navegação em ‘tabs’ na parte de baixo da tela e uma navegação em ‘tabs’ diferente na parte de cima?

Simples, as ‘tabs’ em baixo são de mais fácil acesso, nosso dedão consegue acessá-las facilmente, portanto, elas são usadas como um menu para caminhar pela aplicação.

Já as ‘tabs’ de cima, de mais difícil acesso, são escolhidas por afinidade e possuem a opção de mudar de tela simplesmente arrastando o dedo pro lado. Perceba que as telas são referentes à mesma coisa, ou seja, referentes a determinado menu que selecionamos embaixo.

No nosso exemplo, estamos no menu ‘Seguidores’ e possuímos 2 telas, ‘quem está me seguindo’ e ‘o que meus seguidores estão fazendo’, 2 coisas relacionadas à ‘Seguidores’.

Estruture suas telas adequadamente, pensando também na organização do código. Podemos colocar todas as telas no mesmo StackNavigator, mas teremos um arquivo gigante de difícil manutenção, entretanto, podemos separar nossos StackNavigators por afinidade.

Exemplo: um StackNavigator que cuida da autenticação teria 3 telas: ‘Login’, ‘Cadastro’ e ‘Esqueci minha senha’.

Outro StackNavigator poderia conter o resto da aplicação (se ela for muito pequena).

Nesse caso podemos utilizar um SwitchNavigator com os 2 StackNavigators dentro para controlar toda a aplicação.

Em questão de código, não existem muitas maneiras de melhorar consideravelmente o desempenho do React Navigation além das melhorias que já fizemos nas dicas anteriores, porém, com uma estrutura enxuta, coesa e de fácil entendimento (tanto para o usuário quanto para o desenvolvedor) podemos melhorar consideravelmente nosso aplicativo.

Por fim, É muito interessante usar InteractionManager com React Navigation, no próximo tópico abordaremos esse assunto.

7. Usar o NativeDriver e o InteractionManager para animações

No React Native temos 2 threads, a thread nativa e a thread JavaScript, a maior parte da sua aplicação roda na thread JavaScript, portanto, se conseguirmos deixar as operações pesadas, como as animações, rodando no lado nativo, certamente teremos uma aplicação mais performática.

O NativeDriver simplesmente diz para as nossas animações que elas devem ser executadas no lado nativo.

Como usar o NativeDriver:

Simples assim.

Um dos problemas mais comuns no React Native é a demora nas transições de tela. Podemos melhorar esse problema se esperarmos as animações de transição acabarem, para somente então realizarmos operações mais pesadas, como por exemplo: buscar dados numa API para mostrar em tela.

O mesmo conceito pode ser usado dentro do componentDidMount

8. Usar FlatList e SectionList ao invés de ListView

De acordo com a documentação do React Native, a ListView não deve mais ser utilizada, devido a sua baixa performance e diversos outros problemas.

Como e onde usar cada uma:

FlatList: usar quando temos um vetor de dados.

Basicamente temos:
- dados (data),
- uma função para renderizar o item (renderItem)
- e uma função que extrai uma chave única (keyExtractor), pois no React todo objeto dentro de um map ou de uma List precisa de uma chave única.

A FlatList aceita outras propriedades também, considere dar uma passada na documentação oficial.

Exemplo simples de uma FlatList

SectionList: usar quando temos dados que queremos exibir em seções, ou seja, um vetor de vetores seria ideal.

Basicamente temos:
- seções com dados (sections),
- uma função de renderizar o item (renderItem)
- uma função de renderizar os títulos das seções (renderSectionHeader)
- e uma função que extrai uma chave única (keyExtractor),

A SectionList também aceita outras propriedades, considere dar uma passada na documentação oficial.

Exemplo simples de uma SectionList

Existe também a VirtualizedList, que a própria documentação do React Native sugere usar somente em casos onde é necessária mais flexibilidade que uma FlatList. Considere estudar os benefícios e a necessidade da VirtualizedList antes de usá-la.

9. Cuidar bem das suas imagens

O React Native, por padrão, suporta diferentes densidades de pixel, resumidamente: em uma tela com densidade 2x, teremos muito mais pixels em um mesmo espaço físico do que em uma tela comum.

Vamos supor que nossa tela tem 100x100 pixels, uma imagem de 100x100 pixels cobriria a tela inteira, porém se a nossa tela possuir densidade 2x (200x200), nossa imagem não cobrirá toda a tela e/ou terá a sua qualidade prejudicada.

Como resolvemos esse problema?

Não é muito legal termos uma imagem gigante e redimensioná-la para se adequar a um dispositivo menor, acabamos gastando processamento de forma desnecessária.

Bom, é simples. O React Native já faz isso por nós automaticamente, basta fornecermos imagens adequadas, com o sufixo ‘@2x’ e ‘@3x’ para cada densidade de tela.

Forneceremos 3 imagens iguais com resoluções diferentes:

Logo.png = 100x100

Logo@2x.png = 200x200

Logo@3x.png = 300x300

Na hora de importar a imagem, simplesmente importamos o Logo.png e o React Native cuida de fornecer a melhor imagem conforme a densidade da tela do usuário.

Essa dica não é específica de performance, mas resolvi trazê-la também devido aos benefícios para sua aplicação.

Outra dica é usar alguma biblioteca que fornece cache de imagens, pré-carregamento e outros benefícios como a biblioteca react-native-fast-image.

E por último, se possível, redimensione suas imagens (no Back-end) antes de exibi-las. As imagens com uma resolução muito grande acabam por pesar muito, dificultando a vida e consumindo os dados de usuários que estão fora do Wi-Fi.

10. Usar as versões mais recentes do React Native

Sempre busque utilizar as versões mais recentes do React Native, além das melhorias na performance, cada versão traz diversos benefícios pro desenvolvedor.

Por exemplo, a versão 0.59 trouxe os React Hooks (uma nova forma de escrever Stateful Components) e a versão 0.60 traz uma nova engine chamada Hermes (que promete trazer muita velocidade e uma APK menor), suporte ao AndroidX e AutoLinking (diga adeus ao react-native link que nem sempre funcionava direito), além de diversas outras melhorias.

Enfim, essas foram minhas 10 dicas de como melhorar a performance da sua aplicação construída com React Native, resolvi não focar tanto em explicar o código, mas sim em explicar o porquê das coisas, espero que tenham gostado.

--

--

Lucas Pinheiro

Technology Lover, JavaScript Enthusiastic, currently working as a Full Stack JavaScript Developer at Numenu. Insta: @lucasss_pinheiro