Alura > Cursos de Front-end > Cursos de React > Conteúdos de React > Primeiras aulas do curso React: técnicas avançadas de otimização e desempenho

React: técnicas avançadas de otimização e desempenho

Entendendo renderizações e performance - Apresentação

Apresentando o instrutor e o curso

Olá, estudante da Alura. Sou o instrutor Pedro.

Audiodescrição: Pedro é uma pessoa branca, com cabelos escuros e barba escura. Ele veste roupas pretas em um ambiente iluminado pelas cores azul e rosa.

Seja bem-vindo a mais um curso de React na plataforma da Alura. Recentemente, recebi uma ligação do pessoal da Zoop, um e-commerce de multi-departamento que oferece eletrônicos, moda, itens para casa e jardim, entre outros. Trata-se de uma mega-store que está enfrentando problemas de performance na aplicação. Eles relataram que a aplicação trava em alguns momentos e apresenta problemas de carregamento. Por isso, entraram em contato conosco, especialistas em resolver problemas e ensinar através deles, para ver se conseguimos ajudá-los.

Explicando os pré-requisitos e o foco do curso

Este curso faz parte de uma sequência de conhecimento que é essencial para avançarmos nesta etapa. É imprescindível já conhecer o funcionamento do TypeScript, do React, o funcionamento de componentes por classe, o gerenciamento de estado a partir do context, e também a utilização de hooks e hooks customizados. Vamos focar bastante na parte de performance utilizando ferramentas que serão apresentadas ao longo do curso.

Explorando o ciclo de desenvolvimento no React

Neste curso, vamos entender como funciona o ciclo de desenvolvimento dentro do React, passando pelo ciclo de vida de um componente, como ocorre a comparação de elementos para serem renderizados em tela, como o React entende e administra isso, e também formas de otimizar através de ferramentas do próprio React para lidar com problemas de renderização, como a renderização em cascata. Vamos explorar esses conselhos dentro da nossa sequência de vídeos.

Apresentando ferramentas para otimização de performance

Além disso, conheceremos duas ferramentas muito úteis para gerar relatórios e entender problemas de performance e gargalos dentro da aplicação. Faremos tudo do zero, desde a configuração e instalação, consultando a documentação para entender o funcionamento. Tudo está preparado com muito cuidado para avançarmos passo a passo e concluirmos a jornada com um projeto bem estruturado e relatórios positivos sobre o Zoop.

Concluindo com expectativas futuras

No final, vamos entender o futuro da otimização de performance que a equipe do React preparou, ouvindo a comunidade, como sempre fazem. Esperamos que continuem escutando, pois essas melhorias têm sido muito positivas para nós, enquanto pessoas desenvolvedoras.

Preparem-se, pois na sequência vamos entrar na parte de entender os conceitos do ciclo de vida de um componente e como isso afeta as renderizações. Nos vemos na sequência para começarmos com entusiasmo e compreendermos as demandas do Zoop. Até lá!

Entendendo renderizações e performance - Aprendendo sobre renderizações e custos de performance

Introduzindo o ciclo de vida dos componentes React

Antes de entrarmos na parte do código, vamos entender como funciona a questão de renderização e os custos de performance dentro das nossas aplicações React, especialmente quando tratamos do tema de renderizações. Primeiramente, precisamos compreender o ciclo de vida de um componente. No contexto do React, quando falamos de estrutura de renderização e telas, a menor estrutura que temos dentro de uma página é justamente um componente.

Antes de discutirmos como ocorre a renderização no React, é importante entender o que acontece com um componente desde o momento em que é criado até o momento em que é desmontado ou removido da tela. Na tela, estamos exibindo um modelo simplificado com as três etapas principais de um componente: a montagem (ou mounting), a atualização (updating) e a desmontagem (unmounting). Essa imagem foi adaptada para a nossa paleta de cores, mas pode ser encontrada na documentação do React. Optamos por apresentá-la aqui para facilitar a compreensão.

Explicando a montagem do componente

Para que um componente seja montado na tela, ele passa por três funções que permitem sua montagem, ou seja, sua renderização e o início do ciclo de vida. A primeira etapa é quando o componente passa a existir dentro do DOM, sendo renderizado na tela. O constructor é uma função dos componentes em classe do React, que continua existindo mesmo em componentes funcionais. Nos componentes funcionais, o constructor, render e outras funções dos componentes em classe são encapsulados pelo React para permitir a renderização funcional com as mesmas vantagens dos componentes de classe.

Vamos começar com a primeira etapa do ciclo de vida, que é a montagem do componente. Aqui estão as funções principais envolvidas:

constructor()
render()
componentDidMount()

O constructor recebe as propriedades principais do componente. A função render, que hoje é representada pelo return nos componentes funcionais, determina o que será exibido na tela. A terceira função é o componentDidMount, que atualmente é relacionado ao useEffect, a função de efeito colateral dentro do React. Embora estejamos falando de componentes, isso também se aplica a estruturas maiores, como páginas ou views aninhadas.

Detalhando a atualização e desmontagem do componente

Com as funções constructor, render e componentDidMount, temos o ciclo inicial de renderização do componente na tela. No constructor, definimos propriedades e funções; no render, determinamos o que será exibido; e no componentDidMount, executamos operações após a montagem do componente na tela, como no caso do useEffect.

Após a fase de montagem, temos a fase de atualização (updating), que pode ou não ocorrer, dependendo de alterações nas props ou no state do componente. Se não houver alterações, não há atualização. Quando ocorre uma atualização, o render é chamado novamente, e o componentDidUpdate é executado para garantir que o componente foi renderizado e montado no DOM. Nesse momento, podemos executar efeitos colaterais.

render()
componentDidUpdate()

Após a fase de atualização, temos a fase de desmontagem (unmounting). Quando queremos remover um componente da tela, seja por navegação ou condições específicas, utilizamos o componentWillUnmount. Essa função faz parte do ciclo de vida e verifica se o componente será desmontado. O React, então, trata de remover o elemento do DOM através de um algoritmo de comparação interno, sem que precisemos nos preocupar com isso. Se o componente for desmontado, ele é removido da memória e do DOM visível, mas não do DOM virtual do React.

componentWillUnmount()

Resumindo o ciclo de vida e introduzindo a renderização em cascata

Resumindo o ciclo de vida de um componente: ele é montado na tela, pode ou não receber atualizações que geram novas renderizações, e é desmontado a partir do componentWillUnmount, onde são feitas as validações e a destruição do componente na tela.

Lembramos que estamos discutindo o contexto de classes, pois atualmente, em nossos componentes funcionais, não precisamos lidar com essas funções diretamente. Isso ocorre porque tudo está encapsulado dentro do próprio React, que evoluiu significativamente. Percebemos o quanto a chegada dos hooks aliviou nosso trabalho, assim como as funções que temos dentro do React para construir nossos componentes em tela.

Esta é uma estrutura simples de como funciona o ciclo de vida de um componente. A partir disso, temos um exemplo mais prático. Suponhamos que temos nosso aplicativo ao centro, e nele, dois renders diferentes, ou seja, dois renders filhos do nosso aplicativo. Um deles está gerando um inspiration generator, que pode ser qualquer componente, e, como irmão desse inspiration generator, está renderizando nosso componente fancy text. Dentro do inspiration generator, temos mais dois filhos: a renderização do fancy text e a renderização do copyright à direita. Esta é uma estrutura simples para ilustrar o funcionamento mais detalhado de como isso seria dentro de uma aplicação, não apenas no esquema de montagem, atualização e desmontagem, mas já trazendo para um contexto de aplicativo para avançarmos mais na questão de renderização.

Explorando a renderização em cascata e seus impactos

O que acontece é que, como temos componentes alinhados, a re-renderização ocorre em cascata. Quando temos uma renderização, voltando ao nosso esquema de montagem, atualização e desmontagem, ela acontece em forma de cascata. Se temos um componente pai que foi renderizado, ele também renderizará seus componentes filhos. Ou seja, se houver uma alteração no componente pai, como no caso do inspiration generator, também haverá uma renderização de seus filhos. Isso garante que nossa interface reflita o estado mais recente, evitando defasagem de propriedades, estados e dados que estão transitando dentro dos nossos componentes.

Para ilustrar, quando o inspiration generator é atualizado, ele também renderiza o fancy text e o copyright que estão alinhados a ele. O componente à direita do inspiration generator, o outro fancy text, não sofre alteração porque não está alinhado ao inspiration generator, ou seja, não é um componente filho e, portanto, não participa dessa renderização em cascata.

Abordando problemas de performance e renderizações desnecessárias

Quando temos essa renderização em cascata, que é do próprio React, não há problema na forma como isso funciona. No entanto, quando começamos a falar em tópicos de performance e estabilidade, enfrentamos um problema crítico: renderizações desnecessárias. Isso ocorre porque o React não sabe quais filhos dependem ou não daquele estado que foi alterado. Assim, quando o inspiration generator sofre uma alteração, o React não sabe se o fancy text e o copyright dependem ou não daquela propriedade ou estado alterado. Para evitar problemas e garantir que a interface reflita o estado mais recente, ele renderiza os dois novamente, mesmo que isso não seja necessário.

Começamos a enfrentar problemas significativos quando temos muitos componentes alinhados sofrendo com essa parte de renderização em cascata. Por exemplo, no código, temos uma função App com dois useState de counter, count1 e count2, que são separados, e no return temos nosso título. Vamos trazer isso para nossa estrutura, lembrando da forma como funciona por baixo dos panos. Temos o constructor, o render e o componentDidMount. Na renderização do código, nosso useState, count1 e count2, seriam nosso constructor, e a função de return seria nosso render.

Demonstrando um exemplo prático de renderização

Aqui está um exemplo de como isso é implementado em um componente funcional:

// Componente pai renderizando múltiplos filhos
function App() {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);

    return (
        <>
            <Title /> {/* Título estático */}
            <Counter count={count1} onClick={() => setCount1(count1 + 1)} />
            <Counter count={count2} onClick={() => setCount2(count2 + 1)} />
        </>
    );
}

A cada clique, como não temos componentWillMount, componentDidUpdate, nada disso em nossos componentes de função, ao executar o onClick, por exemplo, no counter, seja count1 ou count2, dentro do useState, ele aciona o componentDidMount, componentDidUpdate e assim por diante, pela lógica existente no useState. Se alterarmos o count1, o filho sofre uma alteração, mas o React não sabe que o counter, nesse caso, teve essa alteração. O React não sabe se todos os outros componentes dependem ou não daquele estado ou propriedade. Como o estado está dentro do App, e não é interno ao counter ou ao count2, por exemplo, ao fazer uma alteração direta no count1, que está dentro do estado interno do App, ele renderiza todo o componente App e todos os filhos, pois o React realmente não sabe se, por exemplo, há outro counter utilizando o count1, ou se estamos utilizando o count1 para alguma informação abaixo. Por isso, ele verifica a alteração e renderiza tudo o que está dentro, facilitando o problema e mantendo a coesão dos dados sendo passados.

Discutindo os impactos de performance em aplicações complexas

Começamos a perceber impactos visíveis dentro da nossa aplicação, principalmente quando lidamos com listas, como será o caso do nosso Zoop, onde teremos várias listagens e componentes que enfrentarão problemas de carregamento por conta disso. A re-renderização de componentes não afetados pelo estado acaba degradando a performance da nossa aplicação, prejudicando nosso andamento durante o desenvolvimento. Se temos muitas listas, muitos componentes com gráficos, e fazemos uma alteração que não necessariamente precisa ser renderizada novamente, enfrentamos uma degradação da performance e relatos de usuários dizendo que a aplicação está lenta e as imagens demoram para carregar. Talvez já tenhamos ouvido isso em nosso local de trabalho, estágio ou projeto em desenvolvimento. Isso é muito comum e um dos erros que enfrentamos no dia a dia, especialmente lidando com informações de um time de marketing, onde há muitas alterações, principalmente quando eventos grandes estão chegando. O fluxo de alterações de informações é muito grande. Se a aplicação não estiver preparada e utilizando os recursos corretos para lidar com isso, o resultado é uma aplicação que começa a travar e relatos negativos de performance que precisamos considerar.

Entendendo renderizações e performance - Primeira avaliação de renderizações

Explorando o arquivo package.json

Vamos começar entendendo como está funcionando o projeto antes de analisarmos o código. Para isso, é importante verificarmos o arquivo package.json da aplicação, que nos mostra como as coisas estão configuradas. Neste momento, estamos compartilhando o editor de texto e abrindo o arquivo package.json, localizado na raiz do projeto.

Aqui está o conteúdo do arquivo package.json:

{
  "name": "react-zoop-ecomm",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "lint:fix": "eslint . --ext ts,tsx --fix",
    "format": "prettier --write \"src/**/*.{ts,tsx,jsx,json,css,md}\"",
    "format:check": "prettier --check \"src/**/*.{ts,tsx,jsx,json,css,md}\"",
    "preview": "vite preview",
    "server": "json-server --watch server.json --port 3001"
  },
  "dependencies": {
    "@emotion/react": "^11.14.0",
    "@emotion/styled": "^11.14.1",
    "@mui/icons-material": "^7.2.0",
    "@mui/material": "^7.2.0",
    "@types/react-router-dom": "^5.3.3",
    "json-server": "^1.0.0-beta.3",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-icons": "^5.5.0",
    "react-router-dom": "^7.6.3"
  },
  "devDependencies": {
    "@eslint/js": "^9.30.1",
    "@types/react": "^19.1.8",
    "@types/react-dom": "^19.1.6",
    "@vitejs/plugin-react": "^4.6.0",
    "autoprefixer": "^10.4.21",
    "eslint": "^9.30.1",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-prettier": "^5.1.3",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.20",
    "globals": "^16.3.0",
    "postcss": "^8.5.6",
    "prettier": "^3.2.5",
    "tailwindcss": "^3.4.17",
    "typescript": "^5.8.3",
    "typescript-eslint": "^8.35.1",
    "vite": "^7.0.3"
  }
}

Configurando o ambiente de desenvolvimento

Caso desejem fazer um checkout da branch e acompanhar o desenvolvimento do projeto durante a aula, o link para acessar o repositório e a branch da aula está sempre no topo do terminal integrado. Para seguir o código da aula, basta verificar o topo do terminal. Se preferirem acessar o repositório após a finalização das aulas, o link é disponibilizado no início de cada nova aula, junto com o material das aulas passadas.

O projeto está utilizando o Vite, conforme indicado pelas dependências, e o React na versão 19.1, que é a mais recente. Vamos abordar um detalhe interessante que o React 19 disponibiliza a partir dessa versão. Observamos que na linha 22 está sendo utilizado o JSON Server, indicando que há um back-end funcionando localmente, provavelmente através de um arquivo .json, que no caso é o server.json na raiz do projeto.

A configuração é padrão para um projeto Vite, com TypeScript, ESLint, PostCSS, Prettier e Tailwind. Além do ESLint, temos os autofixers funcionando, o que demonstra que o projeto possui um certo nível de maturidade e foi bem desenvolvido, oferecendo algumas seguranças para a pessoa desenvolvedora. Além disso, está utilizando bibliotecas como PostCSS, Material e Emotion.

Executando o projeto localmente

Nos comandos, verificamos que o Vite está sendo utilizado para o build. Na linha 14, temos o comando para rodar o servidor local e o run dev para executar o projeto. Localmente, vamos digitar npm run server e duplicar uma aba para digitar npm run dev, permitindo visualizar o projeto rodando no navegador.

npm run server
npm run dev

Após dar um refresh na página, estamos exibindo o Google Chrome no localhost:5173, que é a porta onde o projeto está rodando. Observamos que algumas imagens carregam e outras não, o que pode indicar um problema na configuração das imagens. Os componentes estão bem desenvolvidos, com páginas como "nossa história", "sobre nós", "carrinho" (mostrando o estado de carrinho vazio) e "favoritos" (ainda sem produtos favoritos).

Identificando problemas de performance

No entanto, ao voltar para a tela inicial da aplicação Zoop, notamos uma demora no carregamento das imagens, o que não é um comportamento esperado para uma aplicação de e-commerce. Quando fomos contatados para trabalhar nesse projeto, identificamos problemas de performance, especialmente no tratamento de imagens. O carregamento da listagem está um pouco estranho.

Ao explorar mais, clicamos em uma categoria, como "esportes", e notamos que a lista demora para renderizar as imagens. A listagem não é muito grande, mas preocupa a falta de uma tratativa de paginação ou lazy load, que é um conceito utilizado para exibir informações conforme são renderizadas na tela, auxiliando na virtualização e poupando problemas de renderização.

Analisando problemas de renderização

Detectamos problemas de renderização e re-renderizações desnecessárias, principalmente pelo tempo que as imagens levam para carregar. Ao favoritar um item, ele atualiza na aba "sobre nós" no topo do header, próximo ao ícone do carrinho. O pop-up de desconto está funcionando bem, mas ao fechar, notamos que os itens mais procurados da semana, como bicicleta, sabonete, sapato social e camisa social, mudam ao clicar em "ganhar desconto".

Jogo de panelas, livro de história, bola de futebol americano e bola de golfe. Vamos fechar o pop-up de desconto. Agora, mudou novamente: livro de autoajuda, cortina blackout, sofá de três lugares e livro de arte.

Explorando a estrutura do projeto

Temos aqui um problema clássico de renderização em cascata. Provavelmente, na estrutura que vamos analisar em mais detalhes em breve, qualquer alteração no estado nesta página principal está causando efeitos colaterais em toda a página. Como se trata de uma listagem de itens e possivelmente um retorno aleatório de alguns itens, o estado não está sendo mantido adequadamente para os componentes aninhados, o que resulta em um problema de renderização.

Como vimos no vídeo passado, ao entender mais sobre esses problemas, conseguimos identificar alguns pontos importantes para começarmos a trabalhar. Vamos voltar ao editor de texto, fechar o package.json por enquanto e minimizar os terminais integrados do editor.

No diretório src, temos a estrutura atual do nosso componente. A pasta "assets" contém os recursos, a pasta "componentes" contém alguns componentes, e a pasta "context" contém as APIs de contexto que funcionam internamente na aplicação. Temos também os hooks separados para useCart, categorias e favoritos.

Investigando a página inicial

A página que estamos procurando é a nossa home. Vamos abri-la antes de mexer em outras partes. Na linha 11, temos um componente funcional padrão: export function Home. Aqui, há gerenciamento de estados, como showWelcome, e já conseguimos identificar problemas semelhantes aos que vimos no slide.

Lembram-se do exemplo do count, count1 e count2? O que acontece é que, na home, temos vários outros componentes alinhados. Quando fazemos uma alteração no estado do showWelcome, que é uma alteração interna de estado, ele renderiza novamente todos os componentes filhos, graças à forma como o React trabalha no algoritmo de comparação.

Modificando a lógica de renderização

Analisando mais a fundo, temos outro gerenciamento de estado para a categoria selecionada, um useNavigate, um useCategories, que são hooks customizados, e funções como randomOpen e randomClose. Há várias funções que estão sendo executadas como ifs, chamadas imediatamente após a inicialização. Qualquer alteração nesta página, como nos filtros, faz com que todas essas funções, como a de produtos aleatórios na linha 42 e a de produtos na linha 49, sejam renderizadas novamente devido à forma como estão implementadas.

Vamos fazer uma alteração para testar o randomProduct. Vou remover o if na linha 47, que é a função que estamos chamando novamente, e na linha 42 também vou remover o símbolo do if. Agora, na linha 42, temos apenas o randomProducts. Precisamos alterar onde ele está sendo utilizado, que será na linha 37. Em vez de uma concha, agora estamos passando como uma função. Vamos atualizar.

Testando as alterações no código

Voltando à página, observamos que agora não funciona mais. Isso ocorre porque a forma como desenvolvemos a função e a lógica utilizada, provavelmente fornecida pelo pessoal da OPCON, está interligada em dependências. O products.length está retornando zero em algum momento, e a função agora é chamada apenas uma vez. Vou adicionar um console.log para identificar rapidamente.

No console do navegador, ao atualizar a página, percebemos que a função handleProducts não está sendo chamada da forma que colocamos. Na verdade, o handleProducts está na linha 337, mas alteramos na linha 338, e também precisamos alterar na linha 336.

Após atualizar novamente, observamos que a função foi chamada quatro vezes no total, mas ainda exibe "nenhum produto encontrado" nos mais procurados da semana. Isso ocorre porque, em algum momento, a função não encontra produtos devido à forma como está filtrando. Ao remover os filtros, os mais procurados da semana exibem as informações. Se alterarmos o estado novamente, ele faz essas alterações.

Considerando estratégias de otimização

A forma como o RenderProducts foi desenvolvido depende dos produtos. Toda vez que algo é alterado na aplicação, os produtos são renderizados novamente, chamando a função RenderProducts com os produtos alterados. Vamos começar a entender algumas estratégias para evitar problemas como esse na aplicação.

Esse foi apenas um exemplo. Por enquanto, deixaremos o código assim, pois está funcionando da mesma forma que antes. Vamos fazer uma pausa rápida para continuar investigando o que está acontecendo no código.

Sobre o curso React: técnicas avançadas de otimização e desempenho

O curso React: técnicas avançadas de otimização e desempenho possui 281 minutos de vídeos, em um total de 43 atividades. Gostou? Conheça nossos outros cursos de React em Front-end, ou leia nossos artigos de Front-end.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Escolha a duração do seu plano

Conheça os Planos para Empresas