Componentes eficientes do React: um guia para otimizar o desempenho do React
Tags: react, programming, tutorial
Published: 2022-12-23 14:34:17

A otimização do desempenho de uma aplicação é fundamental para os desenvolvedores que estão preocupados em manter a experiência do usuário positiva e para mantê-los engajados em uma aplicação.

De acordo com a pesquisa da Akamai, um segundo atraso no tempo de carregamento pode causar uma redução de 7% nas conversões, tornando imperativo que os desenvolvedores criem aplicações com desempenho otimizado.

Nas aplicações React, garantimos uma interface do usuário muito rápida por padrão. No entanto, á medida que uma aplicação cresce, os desenvolvedores podem encontrar alguns problemas de desempenho.

Neste guia, discutiremos cinco maneiras importantes de otimizar o desempenho de uma aplicação React, incluindo técnicas de pré-otimização:

- Manter o estado do componente local quando necessário
- Memorizando componentes do React para evitar renderizações desnecessárias
- Divisão de código em React usando dynamic import()

## Técnicas de pré-otimização do React

Antes de otimizar uma aplicação React, devemos entender como o React atualiza sua interface de usuário e como medir o desempenho de uma aplicação. Isso facilita a solução de qualquer problema de desempenho do React.

## Entendendo como o React atualiza a interface do usuário

Quando criamos um componente renderizado, o React cria um DOM virtual para sua árvore de elementos no componente. Agora, sempre que o estado do componente muda, o React recria a árvore DOM virtual e compara o resultado com a renderização anterior:

![Imagem simbolizando uma árvore](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b5ehdezysmzl3qukn1xn.png)

Em seguida, ele atualiza apenas o elemento alterado no DOM real. Este processo é chamado de diferenciação.

O React usa o conceito de um DOM virtual para minimizar o custo de desempenho de renderizar novamente uma página web porque o DOM real é caro para manipular.

Isso é ótimo porque acelera o tempo de renderização da interface do usuário. No entanto, esse conceito também pode desacelerar uma aplicação complexa se não for bem gerenciado.

Essa comparação repetida e renderização de componentes pode ser uma das principais fontes de problemas de desempenho do React em qualquer aplicação React. Construir uma aplicação React em que o algoritmo de diferenciação falha em reconciliar de forma eficaz, fazendo com que toda a aplicação seja renderizada repetidamente, o que pode resultar em uma experiência frustrantemente lenta.

O que podemos deduzir aqui é que uma mudança de estado em um componente React causa uma nova renderização. Da mesma forma, quando o estado passa para um componente filho como uma prop, ele renderiza novamente o filho e assim por diante, o que é bom porque o React deve atualizar a interface de usuário.

Dada uma parte das alterações de dados, o que queremos que o React faça é renderizar novamente apenas os componentes que são diretamente afetados pela alteração (e possivelmente pular até mesmo o processo de comparação para o restante dos componentes):

![Imagem simbolizando uma árvore](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ns9upxg6zyewm2ywfo1p.png)

No entanto, o que o React acaba fazendo é:


![Imagem simbolizando uma árvore](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kguvq644yudni8hootj4.png)

Na imagem acima, todos os nós amarelos são renderizados e diferenciados, resultando em desperdício de tempo/recursos de computação. É aqui que colocaremos principalmente nossos esforços de otimização. O problema surge quando os componentes filhos não são afetados pela mudança de estado. Em outras palavras, eles não recebem nenhuma prop do componente pai.

Mesmo assim, o React renderiza novamente esses componentes filhos. Portanto desde que o componente pai seja renderizado novamente, todos os seus componentes filhos serão renderizados novamente, independente de uma prop passar para eles ou não; este é o comportamento padrão do React.

Vamos demonstrar rapidamente esse conceito. Aqui, temos um componente **App** contendo um estado e um componente filho:

```jsx
import { useState } from "react"

export default function App() {
  const [input, setInput] = useState("")

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <h3>Input text: {input}</h3>
      <ChildComponent />
    </div>
  );
}

function ChildComponent() {
  console.log("componente filho está renderizando")
  return <div>Este é um componente filho.</div>
}
```
Sempre que o estrado do componente **App** é atualizado, **ChildComponent** é renderizado novamente, mesmo quando não é diretamente afetado pela mudança de estado.

[Abra o console nesta demonstração do CodeSandbox](https://codesandbox.io/s/eager-euler-hurvv?file=/src/App.js) e escreva algo no input. Veremos que, para cada pressionamento na tecla, o **ChildComponent** re-renderiza.

Na maioria dos casos, essa nova renderização não deve causar problemas de desempenho e não devemos notar nenhum atraso em nossa aplicação. No entanto, se o componente não afetado renderizar uma computação cara e notarmos problemas de desempenho, devemos otimizar nossa aplicação React.

Isso nos leva a segunda técnica de pré-otimização.

## Profiler da aplicação React para entender onde estão os gargalos

O React nos permite medir o desempenho de nossa aplicação usando o Profiler no ReactDevTools. Lá, podemos coletar informações de desempenho toda vez que a nossa aplicação for renderizada.

O profiler registra quanto tempo leva para renderizar um componente, porque um componente está sendo renderizado e muito mais. A partir daí, podemos investigar o componente afetado e fornecer a otimização necessária.

Para usar o profiler, devemos instalar o React DevTools para nosso navegador de escolha. Se você ainda não o instalou, vá até a página da extensão e instale-a (escolha [Chrome aqui](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) ou [Firefox aqui](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)).

Agora, devemos ver a guia **Profiler** ao trabalhar em um projeto React. 

De volta ao nosso código. Se traçarmos o profiler da aplicação, veremos o seguinte comportamento:

![Iagem animada demonstrativa](https://media.giphy.com/media/N6SOHkCKlXKrMSDw41/giphy.gif)

O profiler do DevTools destaca cada componente renderizado, enquanto o input é atualizado e recebemos todos os detalhes dos componentes renderizados. No gráfico amarelo de baixo, podemos ver quanto tempo demorou para renderizar os componentes e por que o componente **App** está renderizando.


![Imagem de demonstração](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9x1707ayq5ri4nxl2wy3.png)

Da mesma forma, a imagem abaixo mostra que o componente filho está sendo renderizado porque o componente pai foi renderizado.

![Imagem de demonstração](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jp8sf9rimcjyg9nht9wt.png)

Isso pode afetar o desempenho da aplicação React se tivermos uma operação em um componente filho que leva tempo para ser computada. Isso nos leva as nossas técnicas de otimização.

## Técnicas de otimização de desempenho React

1. **Manter o estado do componente local quando necessário**

Aprendemos que uma atualização de estado em um componente pai, renderiza novamente o pai e seus componentes filhos.

Portanto, para garantir que a renderização de um componente ocorra apenas quando necessário, podemos extrair a parte do código que se preocupa com o estado do componente, tornando-a local essa parte do código.

Ao refatorar o código anterior, temos o seguinte:

```jsx
import { useState } from "react";

export default function App() {
  return (
    <div>
      <FormInput />
      <ChildComponent />
    </div>
  );
}

function FormInput() {
  const [input, setInput] = useState("");

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <h3>Input text: {input}</h3>
    </div>
  );
}

function ChildComponent() {
  console.log("child component is rendering");
  return <div>This is child component.</div>;
}
```
Isso garante que apenas o componente que se preocupa com o estado, seja renderizado. Em nosso código, apenas o input se preocupa com o estado. Então, extraímos esse estado e o input para o componente **FormInput**, tornando-o um irmão do **ChildComponent**.

Isso significa que, quando o estado muda no componente **FormInput**, apenas o componente é renderizado novamente.

[Se testarmos a aplicação mais uma vez em nossa demonstração no Codesandbox](https://codesandbox.io/s/mystifying-currying-8cq04?file=/src/App.js), o **ChildComponent** não será mais renderizado a cada pressionamento da tecla. Com esta técnica, podemos melhorar muito o desempenho da nossa aplicação React.

Mas, ás vezes, não podemos evitar ter um estado em um componente global ao passá-lo para componentes filhos como um suporte. Nesse caso, vamos aprender como evitar a renderização novamente dos componentes filhos não afetados.

2. **Memoizar os componentes do React para evitar novas renderizações desnecessárias**

Ao contrário da técnica de desempenho anterior, em que refatorar nosso código nos dá aumento de desempenho, aqui trocamos espaço de memória por tempo. Portanto, devemos apenas memorizar um componente quando necessário.

A memoização é uma estratégia de otimização que armazena em cache uma operação renderizada por componente, salva o resultado na memória e retorna o resultado armazenado em cache para o mesmo input.

Em essência, se um componente filho recebe uma prop, um componente memorizado compara superficialmente a prop por padrão e pula a renderização novamente do componente se a prop não for alterada:

```jsx
import React, { useState } from "react"

// ...

const ChildComponent = React.memo(function ChildComponent({ count }) {
  console.log("componente filho está renderizando")
  return (
    <div>
      <h2>Este é um componente filho.</h2>
      <h4>Count: {count}</h4>
    </div>
  )
})
```
Atualizando o input, tanto o componente **App** quanto o **ChildComponent** são renderizados novamente, que você pode ver no [CodeSandbox](https://codesandbox.io/s/elegant-fast-6nmig?file=/src/App.js).

Em vez disso, **ChildComponent** só deve renderizar novamente ao clicar no botão de contagem, porque deve atualizar a interface do usuário. Aqui, podemos memorizar o **ChildComponent** para otimizar o desempenho da nossa aplicação.

### Usando React.memo()

**React.memo** é um componente higher-order usado para agrupar um componente puramente funcional para evitar a renderização se as props recebidas naquele componente nunca mudarem:

```jsx
import React, { useState } from "react";

export default function App() {
  // ...

  const incrementCount = () => setCount(count + 1);

  return (
    <div>
      {/* ... */}
      <ChildComponent count={count} onClick={incrementCount} />
    </div>
  );
}

const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
  console.log("child component is rendering")
  return (
    <div>
      {/* ... */}
      <button onClick={onClick}>Increment</button>
      {/* ... */}
    </div>
  )
})
```

Se a propriedade count nunca mudar, o React pulará a renderização do **ChildComponent** e reutilizará o resultado renderizado anterior. Portanto, melhorando o desempenho do React.

Você pode [Você pode tentar isso no tutorial no CodeSandbox](https://codesandbox.io/s/black-leftpad-fgrei?file=/src/App.js).

O **React.memo()** funciona muito bem quando passamos valores primitivos, como um número em nosso exemplo. E, se você estiver familiarizado a **igualdade referencial**, os valores primitivos são sempre referencialmente iguais e retornam true se os valores nunca mudarem.

Por outro lado, valores não primitivos como **object**, que incluem arrays e funções, sempre retornam false entre renderizações, porque apontam para diferentes espaços na memória.

Quando passamos **object**, **array** ou **function** como prop, o componente memorizado sempre é renderizado novamente. Aqui, estamos [passando uma função para o componente filho](https://codesandbox.io/s/angry-frost-rl829?file=/src/App.js):

```jsx
export default function App() {
  // ...

  const incrementCount = () => setCount(count + 1);

  return (
    <div>
      {/* ... */}
      <ChildComponent count={count} onClick={incrementCount} />
    </div>
  );
}

const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
  console.log("child component is rendering");
  return (
    <div>
      {/* ... */}
      <button onClick={onClick}>Increment</button>
      {/* ... */}
    </div>
  );
});
```
Este código se concentra na função **incrementCount** que passa para o arquivo **ChildComponent**. Quando o componente **App** renderiza novamente, mesmo quando o botão de contagem não é clicado, a função redefine, fazendo o **ChildComponent** também renderizar novamente.

Para evitar que a função seja redefinida, usaremos um hook **useCallback** que retorna uma versão memorizada do retorno de chamada entre as renderizações.

### Usando o Hook UseCallback

Com o hook **useCallback**, a função **incrementCount** só redefine quando o array de dependência **count** muda:

```jsx
const incrementCount = useCallback(() => setCount(count + 1), [count]);
```
[Você pode experimentá-lo no CodeSandbox.](https://codesandbox.io/s/trusting-fire-ef7i2?file=/src/App.js)

### Usando o Hook UseMemo

Quando a prop que passamos para um componente filho é um array ou objeto, podemos usar um hook **useMemo** para evitar recalcular o mesmo valor caro em um componente. Isso nos permite memorizar esses valores e apenas recalculá-los se as dependências mudarem.

Semelhante a **useCallback**, o **useMemo** também espera uma função e um array de dependências:

```jsx
const memoizedValue = React.useMemo(() => {
  // return expensive computation
}, []);
```
Vamos ver como aplicar o hook useMemo para melhorar o desempenho de uma aplicação React. Dê uma olhada no seguinte código que deixamos intencionalmente lento:

```jsx
import React, { useState } from "react";
 
const expensiveFunction = (count) => {
  // artificial delay (expensive computation)
  for (let i = 0; i < 1000000000; i++) {}
  return count * 3;
};
 
export default function App() {
  // ...
  const myCount = expensiveFunction(count);
  return (
    <div>
      {/* ... */}
      <h3>Count x 3: {myCount}</h3>
      <hr />
      <ChildComponent count={count} onClick={incrementCount} />
    </div>
  );
}
 
const ChildComponent = React.memo(function ChildComponent({ count, onClick }) {
  // ...
});
```
Como podemos ver neste [CodeSandbox](https://codesandbox.io/s/determined-pond-94xg8?file=/src/App.js), experimentamos um atraso em nossa aplicação sempre que tentamos inserir texto no input e quando o botão de contagem é clicado. Isso ocorre porque toda vez que o componente **App** é renderizado, ele invoca **expensiveFunction** e a aplicação se torna mais lenta.

O **expensiveFunction** só deve ser chamado quando o botão de contagem é clicado, não quando digitamos no input. Podemos memorizar o valor retornado de **expensiveFunction** usando o **useMemo**, para que ele recalcule a função apenas quando necessário, ou seja, quando o botão de contagem for clicado.

Para isso, teremos algo assim:

```jsx
const myCount = useMemo(() => {
  return expensiveFunction(count);
}, [count]);
```
3. **Divisão de código em React usando dynamic import()**

A divisão de código, é outra técnica de otimização importante para uma aplicação React. Por padrão, quando uma aplicação React é renderizada em um navegador, um arquivo "pacote" contendo todo o código da aplicação é carregado e exibido aos usuários de uma só vez. Esse arquivo é gerado mesclando todos os arquivos de código necessários para fazer uma aplicação web funcionar.

A ideia de agrupamento é útil porque reduz o número de solicitações HTTP que uma página pode manipular. No entanto, á medida que uma aplicação cresce, os tamanhos de arquivos aumentam, aumentando assim o arquivo do pacote. A certa altura, esse aumento contínuo de arquivos retarda o carregamento inicial da página, reduzindo a satisfação do usuário.

Com a divisão do código, o React nos permite dividir um grande arquivo de pacote em vários pedaços usando dynamic **import()**, seguido pelo lazy loading desses pedaços sob demanda usando o **React.lazy**.

```jsx
import Home from "./components/Home";
import About from "./components/About";
```
E então algo assim:

```jsx
const Home = React.lazy(() => import("./components/Home"));
const About = React.lazy(() => import("./components/About"));
```
Essa sintaxe diz ao React para carregar cada componente dinamicamente. Assim, quando um usuário segue um link para a Home, por exemplo, o React apenas baixa o arquivo para a página solicitada em vez de carregar um grande pacote de arquivos para toda a aplicação.

Após a importação, devemos renderizar os componentes lentos dentro de um componente **Suspense** da seguinte forma: 

```jsx
<React.Suspense fallback={<p>Loading page...</p>}>
  <Route path="/" exact>
    <Home />
  </Route>
  <Route path="/about">
    <About />
  </Route>
</React.Suspense>
```
O Suspense nos permite exibir um texto ou indicador de carregamento como um fallback, enquanto o React espera para renderizar o componente lento na interface do usuário. Você pode [tentar isso sozinho no tutorial do CodeSandbox.](https://codesandbox.io/s/silly-ives-c03ln?file=/src/App.js).

## Conclusão

Como Rob Pike coloca de forma bastante elegante como uma de suas [regras de programação](http://users.ece.utexas.edu/~adnan/pike.html).

> _Medir. Não ajuste a velocidade até ter medido e, mesmo assim, não o faça, a menos que uma parte do código supere o resto._

Não comece a otimizar o código que você acha que pode estar deixando sua aplicação lenta. Em vez disso, deixe as ferramentas de medição de desempenho do React guiá-lo pelo caminho.