Cinco maneiras pelas quais o AirSHIFT melhorou o desempenho do ambiente de execução do app React

Um estudo de caso real sobre a otimização de desempenho de SPAs do React.

Kento Tsuji
Kento Tsuji
Satoshi Arai
Satoshi Arai
Yusuke Utsunomiya
Yusuke Utsunomiya
Yosuke Furukawa
Yosuke Furukawa

O desempenho do site não se resume ao tempo de carregamento. É fundamental oferecer uma experiência rápida e responsiva aos usuários, especialmente para apps de produtividade para computador que as pessoas usam todos os dias. A equipe de engenharia da Recruit Technologies passou por um projeto de refatoração para melhorar um dos apps da Web, o AirSHIFT, e melhorar a performance da entrada do usuário. Confira como eles fizeram isso.

Resposta lenta, menos produtividade

O AirSHIFT é um aplicativo da Web para computador que ajuda proprietários de lojas, como restaurantes e cafés, a gerenciar o trabalho por turnos dos funcionários. Criado com React, o aplicativo de página única oferece recursos avançados para o cliente, incluindo várias tabelas de grade de horários de trabalho organizados por dia, semana, mês e muito mais.

Uma captura de tela do app da Web AirSHIFT.

À medida que a equipe de engenharia da Recruit Technologies adicionava novos recursos ao app AirSHIFT, eles começaram a receber mais feedback sobre a lentidão do desempenho. O gerente de engenharia da AirSHIFT, Yosuke Furukawa, disse:

Em um estudo de pesquisa de usuário, ficamos chocados quando um dos proprietários da loja disse que saía do lugar para preparar café depois de clicar em um botão, apenas para passar o tempo enquanto a tabela de turnos era carregada.

Depois de analisar a pesquisa, a equipe de engenharia percebeu que muitos usuários estavam tentando carregar tabelas de mudança massivas em computadores de baixa especificação, como um laptop Celeron M de 1 GHz de 10 anos atrás.

Movimento de carregamento infinito em dispositivos de baixo custo.

O app AirSHIFT estava bloqueando a linha de execução principal com scripts caros, mas a equipe de engenharia não percebeu o quanto eles eram caros porque estavam desenvolvendo e testando em computadores com especificações ricas e conexões Wi-Fi rápidas.

Um gráfico que mostra a atividade de execução do app.
Ao carregar a tabela de turnos, cerca de 80% do tempo de carregamento foi consumido pela execução de scripts.

Depois de analisar o desempenho no Chrome DevTools com a CPU e o throttling de rede ativados, ficou claro que a otimização de desempenho era necessária. A AirSHIFT formou uma força-tarefa para resolver esse problema. Confira cinco coisas em que eles se concentraram para tornar o app mais responsivo à entrada do usuário.

1. Virtualizar tabelas grandes

A exibição da tabela de turnos exigiu várias etapas caras: construir o DOM virtual e renderizá-lo na tela proporcionalmente ao número de membros da equipe e intervalos de tempo. Por exemplo, se um restaurante tivesse 50 funcionários e quisesse verificar a programação mensal dos turnos, seria uma tabela de 50 (funcionários) multiplicados por 30 (dias), o que levaria a 1.500 componentes de célula para renderização. Essa é uma operação muito cara, especialmente para dispositivos de baixa especificação. Na realidade, as coisas estavam piores. Com a pesquisa, eles descobriram que havia lojas que gerenciavam 200 funcionários,exigindo cerca de 6.000 componentes de célula em uma única tabela mensal.

Para reduzir o custo dessa operação, a AirSHIFT virtualizou a tabela de turnos. Agora, o app só monta os componentes dentro da janela de visualização e desmonta os componentes fora da tela.

Uma captura de tela anotada que demonstra que o AirSHIFT renderizava conteúdo fora da viewport.
Antes: renderização de todas as células da tabela de deslocamento.
Uma captura de tela com anotações que demonstra que o AirSHIFT agora renderiza apenas o conteúdo que está visível na viewport.
Depois: renderização apenas das células dentro da janela de visualização.

Nesse caso, a AirSHIFT usou o react-virtualized, porque havia requisitos para ativar tabelas de grade bidimensionais complexas. Eles também estão buscando maneiras de converter a implementação para usar a react-window leve no futuro.

Resultados

A virtualização da tabela reduziu o tempo de script em 6 segundos (em um ambiente Macbook Pro com lentidão de CPU 4x e 3G rápido). Essa foi a melhoria de performance mais impactante no projeto de refatoração.

Uma captura de tela anotada de uma gravação do painel Performance do Chrome DevTools.
Antes: cerca de 10 segundos de script após a entrada do usuário.
Outra captura de tela anotada de uma gravação do painel Performance do Chrome DevTools.
Após: 4 segundos de script após a entrada do usuário.

2. Fazer auditoria com a API User Timing

Em seguida, a equipe da AirSHIFT refatorizou os scripts executados com base na entrada do usuário. A tabela de chamas do Chrome DevTools permite analisar o que está realmente acontecendo na linha de execução principal. No entanto, a equipe da AirSHIFT achou mais fácil analisar a atividade do aplicativo com base no ciclo de vida do React.

O React 16 fornece o trace de desempenho pela API User Timing, que pode ser visualizado na seção "Timings" do Chrome DevTools. A AirSHIFT usou a seção "Timings" para encontrar lógica desnecessária em execução nos eventos do ciclo de vida do React.

Seção "Timings" do painel "Performance" do Chrome DevTools.
Eventos de tempo do usuário do React.

Resultados

A equipe da AirSHIFT descobriu que uma reconciliação de árvore do React desnecessária estava acontecendo antes de cada navegação de rota. Isso significa que o React estava atualizando a tabela de deslocamentos desnecessariamente antes das navegações. Uma atualização desnecessária do estado do Redux estava causando esse problema. A correção economizou cerca de 750 ms de tempo de script. A AirSHIFT também fez outras microotimizações, o que levou a uma redução total de um segundo no tempo de script.

3. Carregar componentes de forma lenta e mover a lógica custosa para Web Workers

O AirSHIFT tem um aplicativo de chat integrado. Muitos proprietários de lojas se comunicam com os funcionários pelo chat enquanto consultam a tabela de turnos, o que significa que um usuário pode estar digitando uma mensagem enquanto a tabela está sendo carregada. Se a linha de execução principal estiver ocupada com scripts que estão renderizando a tabela, a entrada do usuário poderá ficar instável.

Para melhorar essa experiência, a AirSHIFT agora usa React.lazy e Suspense para mostrar marcadores de posição do conteúdo da tabela enquanto carrega de forma lenta os componentes reais.

A equipe da AirSHIFT também migrou parte da lógica de negócios cara dos componentes carregados de forma lenta para workers da Web. Isso resolveu o problema de instabilidade da entrada do usuário, liberando a linha de execução principal para que ela pudesse se concentrar em responder à entrada do usuário.

Normalmente, os desenvolvedores enfrentam complexidade ao usar workers, mas dessa vez a Comlink fez o trabalho pesado para eles. Confira abaixo o pseudocódigo de como a AirSHIFT automatizou uma das operações mais caras que tinha: calcular o custo total da mão de obra.

Em App.js, use React.lazy e Suspense para mostrar conteúdo substituto durante o carregamento

/** App.js */
import React, { lazy, Suspense } from 'react'

// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))

const Loading = () => (
 
<div>Some fallback content to show while loading</div>
)

/
/ Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
   
return (
   
<div>
     
<Suspense fallback={<Loading />}>
       
<Cost />
     
</Suspense>
    </
div>
 
)
}

No componente de custo, use comlink para executar a lógica de cálculo

/** Cost.js */
import React from 'react';
import { proxy } from 'comlink';

// import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default async function Cost({ userInfo }) {
 
// execute the calculation in the worker
 
const instance = await new WorkerlizedCostCalc();
 
const cost = await instance.calc(userInfo);
 
return <p>{cost}</p>;
}

Implementar a lógica de cálculo executada no worker e a expor com comlink

// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'

// Expose the new workerlized calc function with comlink
expose
({
  calc
(userInfo) {
   
// run existing (expensive) function in the worker
   
return someExpensiveCalculation(userInfo);
 
}
}, self);

Resultados

Apesar da quantidade limitada de lógica que eles transformaram em workers como teste, a AirSHIFT mudou cerca de 100 ms do JavaScript da linha de execução principal para a linha de execução do worker (simulada com limitação de 4x da CPU).

Uma captura de tela de uma gravação do painel Performance do Chrome DevTools que mostra que a criação de scripts agora está ocorrendo em um web worker, e não na linha de execução principal.

A AirSHIFT está atualmente avaliando se é possível carregar outros componentes de forma lazy e transferir mais lógica para workers da Web para reduzir ainda mais o lag.

4. Como definir um orçamento de performance

Depois de implementar todas essas otimizações, era fundamental garantir que o app continuasse a ter bom desempenho ao longo do tempo. O AirSHIFT agora usa bundlesize para não exceder o tamanho atual do arquivo JavaScript e CSS. Além de definir esses orçamentos básicos, eles criaram um painel para mostrar vários percentis do tempo de carregamento da tabela de turnos para verificar se o aplicativo tem bom desempenho mesmo em condições não ideais.

  • O tempo de conclusão do script para cada evento do Redux agora é medido
  • Os dados de performance são coletados no Elasticsearch.
  • A performance do 10º, 25º, 50º e 75º percentil de cada evento é visualizada com o Kibana.

O AirSHIFT agora monitora o evento de carregamento da tabela de turnos para garantir que ele seja concluído em três segundos para os usuários do percentil 75. Por enquanto, esse é um orçamento não aplicado, mas eles estão considerando notificações automáticas pelo Elasticsearch quando excederem o orçamento.

Um gráfico mostrando que o 75º percentil é concluído em cerca de 2.500 ms, o 50º percentil em cerca de 1.250 ms, o 25º percentil em cerca de 750 ms e o 10º percentil em cerca de 500 ms.
Painel do Kibana mostrando dados de performance diários por percentis.

Resultados

No gráfico acima, é possível ver que o AirSHIFT está atingindo o orçamento de 3 segundos para usuários com percentil de 75% e carregando a tabela de turnos em um segundo para usuários com percentil de 25%. Ao capturar dados de desempenho do RUM de várias condições e dispositivos, o AirSHIFT agora pode verificar se uma nova versão do recurso está realmente afetando o desempenho do aplicativo ou não.

5. Hackathons de performance

Embora todos esses esforços de otimização de desempenho tenham sido importantes e impactantes, nem sempre é fácil fazer com que as equipes de engenharia e de negócios priorizem o desenvolvimento não funcional. Parte do desafio é que algumas dessas otimizações de desempenho não podem ser planejadas. Eles exigem experimentação e uma mentalidade de tentativa e erro.

A AirSHIFT está realizando hackathons internos de um dia para que os engenheiros se concentrem apenas no trabalho relacionado à performance. Nessas hackathons, eles removem todas as restrições e respeitam a criatividade dos engenheiros, o que significa que qualquer implementação que contribua para a velocidade deve ser considerada. Para acelerar a hackathon, a AirSHIFT divide o grupo em pequenos grupos, e cada um deles compete para ver quem consegue a maior melhoria na pontuação de desempenho do Lighthouse. As equipes ficam muito competitivas. 🔥

Fotos do hackathon.

Resultados

A abordagem da maratona de programação está funcionando bem para eles.

  • É possível detectar facilmente os gargalos de desempenho testando várias abordagens durante a hackathon e medindo cada uma delas com o Lighthouse.
  • Depois da hackathon, é bastante fácil convencer a equipe de qual otimização ela deve priorizar para o lançamento da produção.
  • Também é uma maneira eficaz de defender a importância da velocidade. Todos os participantes podem entender a correlação entre a forma como você programa e como isso resulta em performance.

Um bom efeito colateral foi que muitas outras equipes de engenharia da Recruit se interessaram por essa abordagem prática, e a equipe da AirSHIFT agora facilita várias hackathons rápidas na empresa.

Resumo

Não foi a jornada mais fácil para a AirSHIFT trabalhar nessas otimizações, mas certamente valeu a pena. Agora, a AirSHIFT carrega a tabela de turnos em 1,5 segundo em média, o que representa uma melhoria de seis vezes em relação ao desempenho antes do projeto.

Depois do lançamento das otimizações de desempenho, um usuário disse:

Agradecemos por fazer a tabela de turnos carregar rapidamente. Agora, organizar o trabalho por turnos é muito mais eficiente.