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

Um estudo de caso real da otimização de desempenho do React SPA.

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

A performance de um 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 e 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 mudança 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 com usuários, ficamos chocados quando uma das proprietárias da loja disse que deixaria seu assento para preparar café depois de clicar em um botão, apenas para passar o tempo de espera pelo carregamento da tabela de turnos.

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.

Ícone de carregamento sem fim em dispositivos mais simples.

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 avançadas 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 criar o perfil do desempenho no Chrome DevTools com a limitação de CPU e rede ativada, 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 aos horários. 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 verdade, as coisas eram piores. A partir da pesquisa, eles descobriram que havia lojas que gerenciam 200 funcionários,exigindo cerca de 6.000 componentes de células 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 era usado para renderizar conteúdo fora da janela de visualização.
Antes: renderização de todas as células da tabela de deslocamento.
Uma captura de tela anotada que demonstra que o AirSHIFT agora renderiza apenas conteúdo visível na janela de visualização.
Depois: renderização apenas das células dentro da viewport.

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 um registro do painel de desempenho do Chrome DevTools.
Antes: cerca de 10 segundos de script após a entrada do usuário.
Outra captura de tela com anotações 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" das Chrome DevTools.
Eventos de velocidade 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. O AirSHIFT também fez outras microotimizações, o que levou a uma redução total de 1 segundo no tempo de scripting.

3. Carregar componentes de forma lenta e mover a lógica cara 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 o Comlink fez o trabalho pesado para eles. Veja abaixo o pseudocódigo de como o AirSHIFT operou uma das operações mais caras da empresa: calcular os custos totais de 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 usada como teste, o AirSHIFT mudou cerca de 100 ms do JavaScript da linha de execução principal para a linha de execução de worker (simulado com uma limitação de CPU de 4x).

Uma captura de tela de uma gravação do painel &quot;Performance&quot; do Chrome DevTools que mostra que o scripting está ocorrendo em um web worker em vez de na linha de execução principal.

O AirSHIFT está analisando se pode carregar lentamente outros componentes e descarregar mais lógica para workers da Web para reduzir ainda mais a instabilidade.

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 o 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 desempenho 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 notar que o AirSHIFT agora está atingindo principalmente o orçamento de 3 segundos para os usuários do 75o percentil e também carregando a tabela de deslocamento em um segundo para os usuários do 25o percentil. 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 o hackathon, o AirSHIFT divide o grupo em pequenas equipes, e cada uma delas compete para ver quem consegue melhorar a pontuação de desempenho do Lighthouse. As equipes ficam muito competitivas! 🔥

Fotos do hackathon.

Resultados

A abordagem do hackathon está funcionando bem para eles.

  • Os gargalos de desempenho podem ser facilmente detectados testando várias abordagens durante o hackathon e medindo cada uma 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 speed hackathons na empresa.

Resumo

Definitivamente, não foi a jornada mais fácil para o AirSHIFT trabalhar nessas otimizações, mas com certeza valeu a pena. Agora, o AirSHIFT carrega a tabela de troca em 1,5 segundo na média, o que é 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 agilizar o carregamento da tabela de turno. Agora, organizar o trabalho por turnos é muito mais eficiente.