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 do React SPA.

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

O desempenho do site não está relacionado apenas ao tempo de carregamento. É fundamental fornecer uma experiência rápida e responsiva aos usuários, especialmente no caso de apps de produtividade para computadores que as pessoas usam todos os dias. A equipe de engenharia da Recruit Technologies passou por um projeto de refatoração para melhorar um de seus apps da Web, o AirSHIFT, para melhorar o desempenho da entrada do usuário. Veja como fizeram isso.

Resposta lenta, menos produtividade

O AirSHIFT é um aplicativo da Web para computadores que ajuda proprietários de lojas, como restaurantes e cafés, a gerenciar os turnos dos funcionários. Criado com o React, o aplicativo de página única oferece recursos avançados para os clientes, incluindo várias tabelas de grade com programações de turnos organizadas 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 um dos proprietários das lojas disse que ela deixaria o lugar para preparar café depois de clicar em um botão, só para acabar com o tempo que estava esperando o carregamento da tabela de turnos.

Após a pesquisa, a equipe de engenharia percebeu que muitos de seus usuários estavam tentando carregar enormes tabelas shift 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 os scripts 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 do tempo de execução do app.
Ao carregar a tabela shift, cerca de 80% do tempo de carregamento foi consumido pela execução de scripts.

Depois de criar o perfil de desempenho no Chrome DevTools com a limitação de CPU e rede ativada, ficou claro que a otimização de desempenho era necessária. O AirSHIFT criou uma força-tarefa para resolver esse problema. Aqui estão cinco aspectos 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: criar o DOM virtual e renderizá-lo na tela proporcional ao número de membros da equipe e aos horários disponíveis. Por exemplo, se um restaurante tivesse 50 membros e quisesse conferir a programação mensal dos turnos, seria uma tabela de 50 (membros) multiplicada por 30 (dias), o que resultaria na renderização de 1.500 componentes de células. 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 gerenciando 200 membros da equipe,exigindo cerca de 6.000 componentes de célula em uma única tabela mensal.

Para reduzir o custo dessa operação, o 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.

Captura de tela com anotação que demonstra que o AirSHIFT foi usado para renderizar conteúdo fora da janela de visualização.
Antes: renderização de todas as células da tabela "shift".
Uma captura de tela com anotação que demonstra que o AirSHIFT agora renderiza somente conteúdo visível na janela de visualização.
Depois: renderizar apenas as células na janela de visualização.

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

Resultados

Virtualizar apenas a tabela reduziu o tempo de script em 6 segundos (em uma lentidão de CPU 4x + ambiente de Macbook Pro limitado pelo 3G rápido). Essa foi a melhoria de desempenho mais impactante no projeto de refatoração.

Uma captura de tela anotada de uma gravação 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 anotada de uma gravação do painel de desempenho do Chrome DevTools.
Depois: quatro segundos de script após a entrada do usuário.

2. Auditoria com a API User Timing

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

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

Seção "Tempos" do painel "Desempenho" do Chrome DevTools.
Eventos de velocidade do usuário do React.

Resultados

A equipe do AirSHIFT descobriu que uma React Tree ativa (link em inglês) desnecessária estava acontecendo logo antes de cada navegação de trajeto. Isso significava que o React estava atualizando a tabela de deslocamento desnecessariamente antes das navegações. Uma atualização de estado do Redux desnecessária estava causando esse problema. A correção economizou cerca de 750ms de tempo de script. O AirSHIFT também fez outras micros otimizações que resultaram em uma redução total de 1 segundo no tempo de scripting.

3. Componentes com carregamento lento e transferência de lógica cara para workers da Web

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

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

A equipe do AirSHIFT também migrou parte da lógica de negócios cara nos componentes de carregamento lento para Web workers (link em inglês). Isso resolveu o problema de instabilidade de 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 no uso de workers, mas desta vez a Comlink fez o trabalho pesado para eles. Abaixo está o pseudocódigo de como o AirSHIFT trabalhou em uma das operações mais caras que tinham: calcular os custos totais da mão de obra.

No 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 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>;
}

Implemente a lógica de cálculo executada no worker e exponha 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 deslocou cerca de 100 ms do JavaScript da linha de execução principal para a linha de execução de worker (simulada com limitação de CPU de 4x).

Uma captura de tela da gravação do painel de desempenho do Chrome DevTools que mostra que o script está ocorrendo em um web worker em vez de na linha de execução principal.

O AirSHIFT está analisando se é possível fazer o carregamento lento de outros componentes e descarregar mais lógica nos web workers 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 permanecesse 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, a equipe criou um painel para mostrar vários percentis do tempo de carregamento da tabela de deslocamentos para verificar se o aplicativo tem desempenho mesmo em condições não ideais.

  • O tempo de conclusão do script para cada evento da Redux agora é medido
  • Os dados de desempenho são coletados no Elasticsearch
  • O desempenho do 10o, 25o, 50o e 75o percentis de cada evento é visualizado com o Kibana

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

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

Resultados

No gráfico acima, é possível notar que o AirSHIFT agora está atingindo principalmente o orçamento de 3 segundos para usuários do 75o percentil e também carrega a tabela de deslocamento em um segundo para usuários do 25o percentil. Ao capturar dados de desempenho RUM de várias condições e dispositivos, o AirSHIFT agora pode verificar se o lançamento de um novo recurso está realmente afetando o desempenho do aplicativo.

5. Hackathons de desempenho

Mesmo que todos esses esforços de otimização de performance tenham sido importantes e impactantes, nem sempre é fácil fazer com que as equipes de engenharia e 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 agora está realizando hackathons internos de performance de um dia para permitir que os engenheiros se concentrem apenas em trabalhos relacionados à performance. Nessas hackatons, eles removem todas as restrições e respeitam a criatividade dos engenheiros, ou seja, qualquer implementação que contribua para a velocidade vale a pena considerar. Para acelerar o hackathon, a AirSHIFT divide o grupo em pequenas equipes, e cada uma delas compete para ver quem consegue a maior melhoria de pontuação de desempenho no Lighthouse. Os times estão muito competitivos! 🔥

Fotos do hackaton.

Resultados

A abordagem do hackathon está funcionando bem para eles.

  • Os gargalos de desempenho podem ser facilmente detectados ao experimentar várias abordagens durante o hackathon e medindo cada uma com o Lighthouse.
  • Após o hackathon, é bastante fácil convencer a equipe sobre qual otimização ela deve priorizar para o lançamento de produção.
  • É também uma maneira eficaz de defender a importância da velocidade. Todos os participantes podem entender a correlação entre como você programa e como isso resulta no desempenho.

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 está viabilizando vários hackatons de velocidade dentro da 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 está carregando a tabela de mudanças em 1,5 segundo, na média, o que representa uma melhoria de seis vezes em relação ao desempenho anterior ao projeto.

Após o lançamento das otimizações de desempenho, um usuário disse:

Agradecemos por acelerar o carregamento da tabela "shift". Organizar o trabalho dos turnos agora é muito mais eficiente.