Um estudo de caso real da otimização de desempenho do React SPA.
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.
À 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.
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.
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.
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.
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.
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).
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.
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! 🔥
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.