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

Publicados: 2022-03-11

Desde a sua introdução, o React mudou a maneira como os desenvolvedores front-end pensam na criação de aplicativos da web. Com o DOM virtual, o React torna as atualizações de interface do usuário o mais eficientes possível, tornando seu aplicativo da web mais rápido. Mas, por que os aplicativos da Web React de tamanho moderado ainda tendem a ter um desempenho ruim?

Bem, a pista está em como você está usando o React.

Uma biblioteca de front-end moderna como o React não torna seu aplicativo magicamente mais rápido. Requer que o desenvolvedor entenda como o React funciona e como os componentes passam pelas várias fases do ciclo de vida do componente.

Com o React, você pode obter muitas melhorias de desempenho que ele oferece medindo e otimizando como e quando seus componentes são renderizados. E o React fornece apenas as ferramentas e funcionalidades necessárias para tornar isso fácil.

Acelere seu aplicativo React otimizando o processo de renderização de seus componentes.

Neste tutorial do React, você aprenderá como medir o desempenho de seus componentes React e otimizá-los para criar um aplicativo Web React com muito mais desempenho. Você também aprenderá como algumas práticas recomendadas de JavaScript também ajudam a fazer com que seu aplicativo Web React ofereça uma experiência de usuário muito mais fluente.

Como funciona o React?

Antes de mergulharmos nas técnicas de otimização, precisamos entender melhor como o React funciona.

No centro do desenvolvimento do React, você tem a sintaxe JSX simples e óbvia e a capacidade do React de construir e comparar DOMs virtuais. Desde seu lançamento, o React influenciou muitas outras bibliotecas front-end. Bibliotecas como Vue.js também contam com a ideia de DOMs virtuais.

Veja como o React funciona:

Cada aplicativo React começa com um componente raiz e é composto por vários componentes em uma formação de árvore. Componentes no React são “funções” que renderizam a interface do usuário com base nos dados (props e estado) que ela recebe.

Podemos simbolizar isso como F .

 UI = F(data)

Os usuários interagem com a interface do usuário e fazem com que os dados sejam alterados. Quer a interação envolva clicar em um botão, tocar em uma imagem, arrastar itens de lista, solicitações AJAX invocando APIs etc., todas essas interações apenas alteram os dados. Eles nunca fazem com que a interface do usuário seja alterada diretamente.

Aqui, dados são tudo o que define o estado do aplicativo da Web, e não apenas o que você armazenou em seu banco de dados. Mesmo bits de estados front-end (por exemplo, qual guia está selecionada ou se uma caixa de seleção está marcada) fazem parte desses dados.

Sempre que houver uma alteração nesses dados, o React usa as funções do componente para renderizar novamente a interface do usuário, mas apenas virtualmente:

 UI1 = F(data1) UI2 = F(data2)

O React calcula as diferenças entre a UI atual e a nova UI aplicando um algoritmo de comparação nas duas versões de seu DOM virtual.

 Changes = Diff(UI1, UI2)

O React então aplica apenas as alterações da interface do usuário à interface do usuário real no navegador.

Quando os dados associados a um componente mudam, o React determina se uma atualização real do DOM é necessária. Isso permite que o React evite operações de manipulação DOM potencialmente caras no navegador, como criar nós DOM e acessar os existentes além da necessidade.

Essa repetida diferenciação e renderização de componentes pode ser uma das principais fontes de problemas de desempenho do React em qualquer aplicativo React. Construir um aplicativo React onde o algoritmo de diferenciação não consegue conciliar efetivamente, fazendo com que o aplicativo inteiro seja renderizado repetidamente pode resultar em uma experiência frustrantemente lenta.

Por onde começar a otimizar?

Mas o que exatamente otimizamos?

Você vê, durante o processo inicial de renderização, o React constrói uma árvore DOM como esta:

Um DOM virtual de componentes React

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):

Reagir renderizando um número ideal de componentes

No entanto, o que o React acaba fazendo é:

Reaja desperdiçando recursos renderizando todos os componentes

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 nossos esforços de otimização principalmente. Configurar cada componente para apenas render-diff quando necessário nos permitirá recuperar esses ciclos de CPU desperdiçados.

Os desenvolvedores da biblioteca React levaram isso em consideração e forneceram um gancho para fazermos exatamente isso: uma função que nos permite dizer ao React quando não há problema em pular a renderização de um componente.

Medindo primeiro

Como Rob Pike coloca elegantemente como uma de suas regras de programação:

A medida. Não ajuste a velocidade até que você tenha medido, e mesmo assim não o faça, a menos que uma parte do código sobrecarregue o resto.

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

O React tem uma ferramenta poderosa justamente para isso. Usando a biblioteca react-addons-perf , você pode obter uma visão geral do desempenho geral do seu aplicativo.

O uso é muito simples:

 Import Perf from 'react-addons-perf' Perf.start(); // use the app Perf.stop(); Perf.printWasted();

Isso imprimirá uma tabela com a quantidade de componentes de tempo desperdiçados na renderização.

Tabela de componentes perdendo tempo na renderização

A biblioteca fornece outras funções que permitem imprimir diferentes aspectos do tempo perdido separadamente (por exemplo, usando as printInclusive() ou printExclusive() ), ou até mesmo imprimir as operações de manipulação do DOM (usando a função printOperations() ).

Levando o benchmarking um passo adiante

Se você é uma pessoa visual, então react-perf-tool é exatamente o que você precisa.

react-perf-tool é baseado na biblioteca react-addons-perf . Ele oferece uma maneira mais visual de depurar o desempenho do seu aplicativo React. Ele usa a biblioteca subjacente para obter medições e as visualiza como gráficos.

Uma visualização de componentes perdendo tempo na renderização

Na maioria das vezes, essa é uma maneira muito mais conveniente de detectar gargalos. Você pode usá-lo facilmente adicionando-o como um componente ao seu aplicativo.

O React deve atualizar o componente?

Por padrão, o React irá rodar, renderizar o DOM virtual e comparar a diferença para cada componente na árvore para qualquer mudança em suas props ou estado. Mas isso obviamente não é razoável.

À medida que seu aplicativo cresce, tentar renderizar novamente e comparar todo o DOM virtual a cada ação acabará ficando mais lento.

O React fornece uma maneira simples para o desenvolvedor indicar se um componente precisa ser renderizado novamente. É aqui que o método shouldComponentUpdate entra em ação.

 function shouldComponentUpdate(nextProps, nextState) { return true; }

Quando esta função retorna true para qualquer componente, ela permite que o processo render-diff seja acionado.

Isso lhe dá uma maneira simples de controlar o processo de diferenciação de renderização. Sempre que você precisar impedir que um componente seja renderizado novamente, simplesmente retorne false da função. Dentro da função, você pode comparar o atual e o próximo conjunto de props e estado para determinar se uma nova renderização é necessária:

 function shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; }

Usando um React.PureComponent

Para facilitar e automatizar um pouco essa técnica de otimização, o React fornece o que é conhecido como componente “puro”. Um React.PureComponent é exatamente como um React.Component que implementa uma função shouldComponentUpdate() com uma prop superficial e comparação de estado.

Um React.PureComponent é mais ou menos equivalente a isso:

 class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState); } … }

Como ele executa apenas uma comparação superficial, você pode achar útil apenas quando:

  • Seus adereços ou estados contêm dados primitivos.
  • Seus adereços e estados têm dados complexos, mas você sabe quando chamar forceUpdate() para atualizar seu componente.

Tornando os dados imutáveis

E se você pudesse usar um React.PureComponent mas ainda tivesse uma maneira eficiente de dizer quando quaisquer props ou estados complexos foram alterados automaticamente? É aqui que as estruturas de dados imutáveis ​​tornam a vida mais fácil.

A ideia por trás do uso de estruturas de dados imutáveis ​​é simples. Sempre que um objeto contendo dados complexos for alterado, em vez de fazer as alterações nesse objeto, crie uma cópia desse objeto com as alterações. Isso torna a detecção de alterações nos dados tão simples quanto comparar a referência dos dois objetos.

Você pode usar Object.assign ou _.extend (de Underscore.js ou Lodash):

 const newValue2 = Object.assign({}, oldValue); const newValue2 = _.extend({}, oldValue);

Melhor ainda, você pode usar uma biblioteca que fornece estruturas de dados imutáveis:

 var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 2); assert(map1.equals(map2) === true); var map3 = map1.set('b', 50); assert(map1.equals(map3) === false);

Aqui, Immutable.Map é fornecido pela biblioteca Immutable.js.

Toda vez que um mapa é atualizado com seu método set , um novo mapa é retornado somente se a operação set tiver alterado o valor subjacente. Caso contrário, o mesmo mapa é retornado.

Você pode aprender mais sobre como usar estruturas de dados imutáveis ​​aqui.

Mais técnicas de otimização de aplicativos React

Usando a compilação de produção

Ao desenvolver um aplicativo React, você recebe avisos e mensagens de erro realmente úteis. Isso torna a identificação de bugs e problemas durante o desenvolvimento uma felicidade. Mas eles têm um custo de desempenho.

Se você olhar no código-fonte do React, verá muitas verificações if (process.env.NODE_ENV != 'production') . Esses pedaços de código que o React está executando em seu ambiente de desenvolvimento não são algo necessário para o usuário final. Para ambientes de produção, todo esse código desnecessário pode ser descartado.

Se você inicializou seu projeto usando create-react-app , você pode simplesmente executar npm run build para produzir a compilação de produção sem esse código extra. Se você estiver usando o Webpack diretamente, poderá executar webpack -p (que é equivalente a webpack --optimize-minimize --define process.env.NODE_ENV="'production'" .

Funções de vinculação antecipadas

É muito comum ver funções vinculadas ao contexto do componente dentro da função de renderização. Este é frequentemente o caso quando usamos essas funções para manipular eventos de componentes filhos.

 // Creates a new `handleUpload` function during each render() <TopBar onUpload={this.handleUpload.bind(this)} /> // ...as do inlined arrow functions <TopBar onUpload={files => this.handleUpload(files)} />

Isso fará com que a função render() crie uma nova função em cada renderização. Uma maneira muito melhor de fazer o mesmo é:

 class App extends React.Component { constructor(props) { super(props); this.handleUpload = this.handleUpload.bind(this); } render() { … <TopBar onUpload={this.handleUpload} /> … } }

Usando vários arquivos de pedaços

Para aplicativos Web React de página única, geralmente acabamos agrupando todo o nosso código JavaScript de front-end em um único arquivo minificado. Isso funciona bem para aplicativos da Web de tamanho pequeno a moderado. Mas à medida que o aplicativo começa a crescer, entregar esse arquivo JavaScript empacotado para o navegador pode se tornar um processo demorado.

Se você estiver usando o Webpack para criar seu aplicativo React, poderá aproveitar seus recursos de divisão de código para separar o código do aplicativo criado em vários "pedaços" e entregá-los ao navegador conforme necessário.

Existem dois tipos de divisão: divisão de recursos e divisão de código sob demanda.

Com a divisão de recursos, você divide o conteúdo do recurso em vários arquivos. Por exemplo, usando CommonsChunkPlugin, você pode extrair código comum (como todas as bibliotecas externas) em um arquivo “chunk” próprio. Usando ExtractTextWebpackPlugin, você pode extrair todo o código CSS em um arquivo CSS separado.

Esse tipo de divisão ajudará de duas maneiras. Ele ajuda o navegador a armazenar em cache os recursos que mudam com menos frequência. Também ajudará o navegador a aproveitar o download paralelo para reduzir potencialmente o tempo de carregamento.

Um recurso mais notável do Webpack é a divisão de código sob demanda. Você pode usá-lo para dividir o código em um pedaço que pode ser carregado sob demanda. Isso pode manter o download inicial pequeno, reduzindo o tempo necessário para carregar o aplicativo. O navegador pode então baixar outros pedaços de código sob demanda quando necessário pelo aplicativo.

Você pode aprender mais sobre a divisão de código do Webpack aqui.

Habilitando o Gzip em seu servidor web

Os arquivos JS do pacote do aplicativo React são geralmente muito grandes, portanto, para tornar a página da Web mais rápida, podemos habilitar o Gzip no servidor da Web (Apache, Nginx, etc.)

Todos os navegadores modernos suportam e negociam automaticamente a compactação Gzip para solicitações HTTP. A ativação da compactação Gzip pode reduzir o tamanho da resposta transferida em até 90%, o que pode reduzir significativamente o tempo de download do recurso, reduzir o uso de dados para o cliente e melhorar o tempo de renderização da primeira página.

Verifique a documentação do seu servidor web sobre como habilitar a compactação:

  • Apache: use mod_deflate
  • Nginx: Use ngx_http_gzip_module

Usando Eslint-plugin-react

Você deve usar o ESLint para quase qualquer projeto JavaScript. Reagir não é diferente.

Com eslint-plugin-react , você estará se forçando a se adaptar a muitas regras na programação React que podem beneficiar seu código a longo prazo e evitar muitos problemas comuns e problemas que ocorrem devido ao código mal escrito.

Torne seus aplicativos Web React mais rápidos novamente

Para aproveitar ao máximo o React, você precisa aproveitar suas ferramentas e técnicas. O desempenho de um aplicativo Web React está na simplicidade de seus componentes. Sobrecarregar o algoritmo de comparação de renderização pode fazer com que seu aplicativo tenha um desempenho ruim de maneiras frustrantes.

Antes de otimizar seu aplicativo, você precisa entender como os componentes do React funcionam e como eles são renderizados no navegador. Os métodos de ciclo de vida do React oferecem maneiras de evitar que seu componente seja renderizado novamente desnecessariamente. Elimine esses gargalos e você terá o desempenho do aplicativo que seus usuários merecem.

Embora existam mais maneiras de otimizar um aplicativo da Web React, ajustar os componentes para atualizar somente quando necessário produz a melhor melhoria de desempenho.

Como você mede e otimiza o desempenho do seu aplicativo Web React? Compartilhe nos comentários abaixo.

Relacionado: Busca de dados obsoletos durante a revalidação com ganchos do React: um guia