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