DEV Community

Cover image for Componentes eficientes do React: um guia para otimizar o desempenho do React
Ivan Trindade
Ivan Trindade

Posted on

Componentes eficientes do React: um guia para otimizar o desempenho do React

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

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

No entanto, o que o React acaba fazendo é:

Imagem simbolizando uma árvore

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:

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>
}
Enter fullscreen mode Exit fullscreen mode

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 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 ou Firefox aqui).

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

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

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

Imagem de demonstração

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:

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>;
}
Enter fullscreen mode Exit fullscreen mode

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, 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.

  1. 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:

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>
  )
})
Enter fullscreen mode Exit fullscreen mode

Atualizando o input, tanto o componente App quanto o ChildComponent são renderizados novamente, que você pode ver no CodeSandbox.

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:

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>
  )
})
Enter fullscreen mode Exit fullscreen mode

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.

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:

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>
  );
});
Enter fullscreen mode Exit fullscreen mode

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:

const incrementCount = useCallback(() => setCount(count + 1), [count]);
Enter fullscreen mode Exit fullscreen mode

Você pode experimentá-lo no CodeSandbox.

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:

const memoizedValue = React.useMemo(() => {
  // return expensive computation
}, []);
Enter fullscreen mode Exit fullscreen mode

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:

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 }) {
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Como podemos ver neste CodeSandbox, 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:

const myCount = useMemo(() => {
  return expensiveFunction(count);
}, [count]);
Enter fullscreen mode Exit fullscreen mode
  1. 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.

import Home from "./components/Home";
import About from "./components/About";
Enter fullscreen mode Exit fullscreen mode

E então algo assim:

const Home = React.lazy(() => import("./components/Home"));
const About = React.lazy(() => import("./components/About"));
Enter fullscreen mode Exit fullscreen mode

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:

<React.Suspense fallback={<p>Loading page...</p>}>
  <Route path="/" exact>
    <Home />
  </Route>
  <Route path="/about">
    <About />
  </Route>
</React.Suspense>
Enter fullscreen mode Exit fullscreen mode

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..

Conclusão

Como Rob Pike coloca de forma bastante elegante como uma de suas regras de programação.

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.

Top comments (0)