Um estudo de caso real sobre a otimização de desempenho de SPAs do React.
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.
À 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.
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.

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.


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. 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).
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.

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. 🔥
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.