React: Eliminando problemas de performance

Suas aplicações React mais rápidas com dicas práticas!

Image for post
Image for post
Créditos da ilustração — LogRocket

Você já ficou pensando como deixar suas aplicações React mais rápidas?

Você não está sozinho! 🤗

E se aparecer uma lista de dicas te dizendo como eliminar problemas comuns de performance em React?

Bom, esse artigo é a resposta para essas perguntas!

Vamos analisar problemas comuns de performance em aplicações React de uma maneira intuitiva e passo-a-passo.

Primeiro, iremos conhecer os problemas e em seguida resolve-los. Ao fazer isso, você irá absorver melhor os conceitos e levar as dicas para suas aplicações do dia-a-dia.

O objetivo desse artigo não é ser uma tese de várias páginas, ao invés disso, iremos discutir pequenas dicas que você pode começar a usar hoje!

Animado? Eu também, bora começar!

Projeto de exemplo

Para deixar esse artigo o mais prático possível nós iremos analisar alguns cenários enquanto trabalhamos em uma aplicação de exemplo.

Iremos chama-la de Cardie.

Image for post
O app Cardie

Você pode acompanhar fazendo o download do código desse repositório no GitHub.

Cardie é uma aplicação simples. Tudo que ela faz é mostrar a informação de um perfil de usuário:

Image for post
Os detalhes do usuário mostrados pelo app

Ela também contém uma funcionalidade onde o usuário pode mudar sua profissão ao clicar em um botão:

Image for post
Atualizando a profissão ao clicar no botão

Eu acredito que você está mentalmente rindo dessa aplicação tão pequena ou pensando “isso não é uma aplicação de verdade”. Você ficará surpreso ao saber que o conhecimento adquirido procurando e isolando problemas de performance nesse exemplo, se aplicam para qualquer aplicação que você virá a construir.

Fique calmo, aperte os cintos e vamos começar nossa lista!

1. Identificando renderizações desnecessárias

Identificar renderizações desnecessárias em aplicações React é o começo perfeito para entender os problemas de performance.

Existem algumas maneiras de abordar esse problema, um método que recomendo é ativar o “highlight updates” no React DevTools.

Image for post
Habilitando "highlight updates" no React DevTools

Ao interagir com seu app, as atualizações serão marcadas na tela com um flash verde. Esse flash verde mostra quais componentes no seu app foram re-renderizados pelo React.

Ao mudar a profissão do usuário, me parece que todo o componente pai <App>, também é re-renderizado:

Image for post
Perceba o piscar de bordas em torno do app

Isso não parece correto…

O app funciona, mas não há razão alguma para todos os componentes serem re-renderizados quando a atualização ocorreu em uma pequena parte.

Image for post
A atualização real acontece em uma pequena parte do app

Uma atualização ideal seria mais ou menos assim:

Image for post
A atualização ideal só mostraria bordas ao redor da profissão

Em aplicações mais complexas, o impacto de re-renderizações desnecessárias pode ser enorme! A re-renderização de um componente pode ser o suficiente para levantar preocupações de performance.

Descobrimos o problema mas qual a solução?

2. Isolando regiões com atualizações frequentes em seus próprios componentes

Ao entender como re-renderizações desnecessárias acontecem na sua aplicação, podemos começar a dividir nossa árvore de componentes e isolar as regiões que tem alta frequência de atualização.

Vamos visualizar o que eu quero dizer.

No Cardie, o componente <App /> está conectado a redux store através da função connect() que vem do react-redux. Nós estamos recebendo name, location, likes e description:

Image for post
<App /> recebe as props direto da redux store

A prop description define a profissão atual do usuário.

O que está acontecendo é que, a qualquer mudança na profissão do usuário realizada pelo apertar do botão, o valor da prop description muda. Essa mudança na prop causa uma re-renderização completa do componente <App />.

Uma regra básica do React é que, a qualquer mudança em props ou state do componente, uma re-renderização é realizada.

Image for post
Image for post
Um componente React renderiza uma árvore de elementos. Esses elementos são definidos através de props e state. Se o valor de prop ou state mudarem, a árvore de elementos é re-renderizada, resultando em uma nova árvore

Ao invés de permitir que o componente <App /> seja re-renderizado de forma desnecessária, poderíamos localizarmos os elementos que estão sendo atualizados e isola-los em um componente específico?

Por exemplo, poderíamos criar um novo componente chamado Profession, que renderiza seus próprios elementos DOM.

Image for post

Nesse caso, o componente Profession irá renderizar a descrição da profissão do usuário:

Agora, o componente <Profession /> será o unico sendo re-renderizado dentro de
Agora, o componente <Profession /> será o único sendo re-renderizado dentro de <App />

Nossa árvore de componente ficará mais ou menos assim:

Image for post
O <App /> renderiza seus elementos e o componente <Profession />

O que é importante notar aqui é que, delegamos a responsabilidade do <App /> pela prop profession para o componente <Profession />.

A profissão será buscada da redux store pelo componente <Profession />
A profissão será buscada da redux store diretamente pelo componente <Profession />

Você usando Redux ou não, o ponto aqui é que <App /> não precisa mais ser re-renderizado pela mudança da prop profession. Ao invés disso, o componente <Profession /> será.

Ao completar essa refatoração, vamos ver os flashes verdes de atualização:

Image for post
Perceba que a borda verde de atualização fica em torno do <Profession />

Para ver as mudanças você pode ver a branch isolated-component no repositório.

3. Usando PureComponente quando apropriado

Qualquer conteúdo sobre performance em React provavelmente irá falar sobre React.PureComponent.

Porém, como e quando sabemos que é correto a utilização de React.PureComponents?

É verdade que você pode transformar todos os componentes em pure components, mas lembre-se que, existe uma razão para que isso não seja o padrão. E isso tem relação com o ciclo de vida shouldComponentUpdate.

A promessa de pure components é que um componente re-renderiza APENAS se suas props e state forem diferentes das props e state anteriores.

Podemos utilizar React.PureComponent para criar esse tipo de componente:

// ao invés de:
class Me extends React.Component {}

// podemos fazer:
class Me extends React.PureComponent {}

Para ilustrar esse caso no nosso app de exemplo, vamos dividir os elementos renderizados por <Profession /> em outros menores.

Versão atual:

... 
const Description = ({ description }) => {
return (
<p>
<span className="faint">I am</span> a {description}
</p>
);
}
...

Iremos mudar para:

const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};

Agora, o componente <Description /> renderiza 4 componentes filhos.

Image for post

Perceba que o componente Description recebe profession como prop e ele passa esse valor diretamente para o componente Profession. Tecnicamente, os outros 3 componentes não precisam se preocupar com esse valor.

O conteúdo desses novos componentes são bem simples. Em <I /> nós estamos retornando <span>I</span> , por exemplo.

Ao rodarmos a aplicação, o resultado será o mesmo.

O que é interessante com essa mudança é que ao alterar o valor de description, TODOS os elementos filhos de <Description /> serão re-renderizados:

Image for post
Quando uma nova prop é passada para <Description />, todos seus elementos filhos são re-renderizados

Podemos adicionar alguns logs no método render de cada elemento filho para podermos visualizar isso:

Image for post

E ao observar os flashes verdes de atualização pelo React DevTools:

Image for post
Perceba as bordas de atualização em torno de <I />, <Am /> e <A />

Esse comportamento é esperado. Em qualquer momento que props ou state mudam, toda a árvore de elementos de um determinado componente estão sendo re-computadas. Isso é sinônimo de re-renderização.

Nesse exemplo em particular, você pode concordar comigo que não faz sentido termos <I />, <Am /> e <A />.

Mas isso está simulando um exemplo real de que, quando a prop de um elemento pai é alterada, nesse caso <Description />, todos seus elementos filhos são re-renderizados. Se isso fosse uma aplicação suficientemente grande, esse comportamento já seria suficiente para alguns problemas de performance.

E se alterarmos esses componentes para React.PureComponent?

Ficando com:

import * as React from "react"

// antes
class I extends React.Component {
render() {
return <span className="faint">I </span>;
}

// depois
class I extends React.PureComponent {
render() {
return <span className="faint">I </span>;
}

Com isso, React é informado que, ao recalcular alguma mudança na árvore de elementos, esses componentes não precisam ser re-computados caso os valores de suas props e state não mudarem.

Exatamente, não re-renderize esses componentes, mesmo se o elemento pai for alterado!

Image for post

Ao analisar as atualizações depois da refatoração, podemos ver que os elementos filhos não estão sendo re-renderizados.

Apenas <Profession /> com sua nova props:

Image for post
Aqui podemos ver que as bordas de atualização em <I />, <Am /> e <A /> não existe mais, apenas no elemento pai como um todo e o text de profissão.

Em grandes aplicações, você irá ver bons resultados ao isolar pure components como nesse exemplo.

Para ver as mudanças você pode ver a branch pure-components no repositório.

4. Evite passar novos objetos como props

Vale lembrar novamente que a qualquer momento que props de um componente for alterada, uma re-renderização acontece.

Image for post
Caso props ou state mudem, uma nova arvore de elementos é renderizada

Mas e se as props de um componente não mudarem e o React pensar que elas mudaram?

Então… teremos uma re-renderização!

Isso é estranho, não é?

Esse comportamento acontece devido a como o JavaScript funciona e como React trata comparações entre props anteriores e novas.

Vou te mostrar um exemplo.

Analisando o conteúdo do componente Description:

const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};

Agora, se alterarmos o componente <I /> para receber uma prop i da seguinte forma:

const i = { 
value: "i"
};

E utilizamos esse valor no componente:

class I extends PureComponent {
render() {
return <span className="faint">{this.props.i.value} </span>;
}
}

E no componente Description passamos a prop i como abaixo:

class Description extends Component {
render() {
const i = {
value: "i"
};
return (
<p>
<I i={i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}

Esse trecho de código está correto e funciona muito bem, mas temos um problema que muitas vezes é difícil de detectar.

Mesmo que o componente <I /> seja um React.PureComponent, ele irá re-renderizar toda vez que o componente <Profession /> for alterado:

Image for post
Ao clicar no botão, os logs mostram uma re-renderização de ambos <I /> e <Profession />. Mas não houve mudança na props de <I />. Porquê essa re-renderização ocorreu?

Porquê isso acontece?

No momento que Description recebe novas props, a função render é chamada para criar a árvore de elementos.

Ao invocar esse função, nós criamos uma nova constante i:

const i = { 
value: "i"
};

Quando o React avalia a linha <I i={i} />, ele entende que i é uma nova props, ou um novo objeto — por isso a re-renderização irá ocorrer!

Voltando ao básico de React, é realizada uma comparação superficial (shallow comparison) entre a props anterior e a próximo.

Para valores escalares, como strings e números, eles são comparados por valor. Para objetos, eles são comparados por referência.

Isso significa que a constante i tem o mesmo valor entre re-renderizações, mas a referência não é a mesma. A posição em memória é diferente.

Um novo objeto é criado a cada chamada do método render(). Por essa razão, a prop passada para <I /> é marcada como "nova", criando uma re-renderização.

Isso pode levar a problemas de desempenho. Esse problema também é válido para props que cuidam de eventos do usuário. Se você pode evitar, não faça o seguinte:

... 
render() {
<div onClick={() => {// código...}}
}
...

Você está criando uma nova função a cada vez que render() é chamado. É melhor fazer:

...
handleClick = () => {}

render() {
<div onClick={this.handleClick}
}
...

Da mesma maneira, podemos refatorar a prop enviada para <I /> da seguinte maneira:

class Description extends Component {
i = {
value: "i"
};
render() {
return (
<p>
<I i={this.i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}

Dessa maneira, a referência para i será sempre this.i e não um novo objeto a cada chamada do método render().

Para ver as mudanças você pode ver a branch new-objects no repositório.

5. Utilize a versão de produção do React

Ao realizar o deploy para produção, sempre configure seu build para utilizar a versão de produção do React. Isso pode ser simples, mas é uma boa prática:

Image for post
Alerta de “development build” que você recebe no React DevTools

Ao criar uma aplicação com create-react-app, você pode utilizar o comando npm build para criar um pacote de arquivos otimizados para produção.

6. Implemente divisão de código

Ao empacotar sua aplicação para produção, você provavelmente terá todo o seu código em um só grande arquivo.

O problema com isso é que seu app irá crescer, assim como esse grande arquivo final.

Image for post

A idéia de divisão de código (code-splitting) é evitar enviar esse grande arquivo para o usuário de uma só vez, carregando partes dinamicamente, conforme o usuário precisa.

Um exemplo comum disso é divisão de código baseado em rotas. Nesse cenário, a de divisão de código é criada baseada nas rotas da sua aplicação.

Image for post

Outra abordagem é baseada em componentes. Nesse método, se um componente não está sendo mostrado para o usuário, o carregamento de seu código pode acontecer posteriormente.

Qualquer um dos métodos que você escolher, é importante entender os prós e contras para não degradar a experiência do usuário na sua aplicação.

Divisão de código é uma ótima idéia e pode melhorar a performance da sua aplicação.

Falamos conceitualmente sobre divisão de código, se você estiver interessado em detalhes técnicos, recomendo dar uma olhada na documentação oficial.

Conclusão

Agora que temos uma lista de dicas descente para buscar e arrumar problemas comuns de performance, bora deixar esses apps mais rápidos! 🎉

⭐️ Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store