Otimização de inicialização do JavaScript

À medida que criamos sites que dependem mais do JavaScript, às vezes pagamos pelo que enviamos de maneiras que nem sempre são fáceis de ver. Neste artigo, vamos explicar por que um pouco de disciplina pode ajudar se você quiser que seu site carregue e seja interativo rapidamente em dispositivos móveis. Enviar menos JavaScript pode significar menos tempo na transmissão de rede, menos tempo gasto na descompactação do código e menos tempo na análise e compilação desse JavaScript.

Rede

Quando a maioria dos desenvolvedores pensa no custo do JavaScript, eles pensam em termos de custo de download e execução. O envio de mais bytes de JavaScript pela rede leva mais tempo quanto mais lenta for a conexão do usuário.

Quando um navegador solicita um
recurso, ele precisa ser buscado e descompactado. No caso
de recursos como JavaScript, eles precisam ser analisados e compilados antes da
execução.

Isso pode ser um problema, já que o tipo de conexão de rede efetiva do usuário pode não ser 3G, 4G ou Wi-Fi. Você pode estar no Wi-Fi de uma cafeteria, mas conectado a um ponto de acesso celular com velocidades 2G.

É possível reduzir o custo de transferência de rede do JavaScript:

  • Enviar apenas o código que o usuário precisa.
  • Minificação
  • Compactação
    • No mínimo, use gzip para compactar recursos baseados em texto.
    • Considere usar o Brotli ~q11. O Brotli supera o gzip na taxa de compactação. Ele ajudou a CertSimple a economizar 17% no tamanho dos bytes de JS compactados e o LinkedIn a economizar 4% nos tempos de carregamento.
  • Remover código não utilizado.
  • Armazenar código em cache para minimizar as viagens de rede.
    • Use o armazenamento em cache HTTP para garantir que os navegadores armazenem as respostas em cache de maneira eficaz. Determine os tempos de vida ideais para scripts (max-age) e forneça tokens de validação (ETag) para evitar a transferência de bytes inalterados.
    • O armazenamento em cache do service worker pode tornar a rede do app resiliente e dar acesso rápido a recursos como o cache de código do V8.
    • Use o armazenamento em cache de longo prazo para evitar a necessidade de buscar novamente recursos que não foram alterados. Se estiver usando o Webpack, consulte hashing de nome de arquivo.

Analisar/compilar

Depois do download, um dos custos mais pesados do JavaScript é o tempo que um mecanismo de JS leva para analisar/compilar esse código. No Chrome DevTools, a análise e a compilação fazem parte do tempo amarelo "Scripting" no painel "Performance".

ALT_TEXT_HERE

As guias "Bottom-Up" e "Call Tree" mostram os tempos exatos de análise/compilação:

ALT_TEXT_HERE
Painel de desempenho do Chrome DevTools > Bottom-Up. Com as estatísticas de chamada de ambiente de execução do V8 ativadas, podemos ver o tempo gasto em fases como análise e compilação

Mas por que isso é importante?

ALT_TEXT_HERE

Passar muito tempo analisando/compilando código pode atrasar muito o tempo que um usuário leva para interagir com seu site. Quanto mais JavaScript você enviar, mais tempo levará para analisar e compilar antes que o site se torne interativo.

Byte por byte, o JavaScript é mais caro para o navegador processar do que uma imagem ou fonte da Web de tamanho equivalente — Tom Dale

Em comparação com o JavaScript, há vários custos envolvidos no processamento de imagens de tamanho equivalente (elas ainda precisam ser decodificadas!), mas, em um hardware móvel comum, o JS tem mais chances de afetar negativamente a interatividade de uma página.

ALT_TEXT_HERE
Os bytes de JavaScript e de imagem têm custos muito diferentes. As imagens geralmente não bloqueiam a linha de execução principal nem impedem que as interfaces fiquem interativas enquanto são decodificadas e rasterizadas. No entanto, o JS pode atrasar a interatividade devido aos custos de análise, compilação e execução.

Quando dizemos que a análise e a compilação são lentas, o contexto é importante. Estamos falando de smartphones comuns. Os usuários comuns podem ter smartphones com CPUs e GPUs lentas, sem cache L2/L3 e que podem até ter restrições de memória.

Os recursos de rede e de dispositivo nem sempre são os mesmos. Um usuário com uma conexão de fibra incrível não tem necessariamente a melhor CPU para analisar e avaliar o JavaScript enviado ao dispositivo. O mesmo vale para o inverso: uma conexão de rede terrível, mas uma CPU extremamente rápida. — Kristofer Baxter, LinkedIn

Abaixo, vemos o custo de analisar cerca de 1 MB de JavaScript descompactado (simples) em hardware de baixo e alto desempenho. Há uma diferença de 2 a 5 vezes no tempo de análise/compilação de código entre os smartphones mais rápidos do mercado e os smartphones comuns.

ALT_TEXT_HERE
Este gráfico destaca os tempos de análise de um pacote de 1 MB de JavaScript (cerca de 250 KB compactado com gzip) em computadores e dispositivos móveis de diferentes classes. Ao analisar o custo da análise, é preciso considerar os números descompactados.Por exemplo, um JS compactado com gzip de ~250 KB é descompactado para ~1 MB de código.

E um site real, como o CNN.com?

No iPhone 8, um dispositivo sofisticado, leva apenas cerca de 4 segundos para analisar/compilar o JS da CNN, em comparação com cerca de 13 segundos em um smartphone comum (Moto G4). Isso pode afetar significativamente a rapidez com que um usuário pode interagir totalmente com o site.

ALT_TEXT_HERE
Acima, vemos os tempos de análise comparando o desempenho do chip A11 Bionic da Apple com o Snapdragon 617 em um hardware Android mais comum.

Isso destaca a importância de testar em hardware médio (como o Moto G4) em vez de apenas no smartphone que você tem no bolso. No entanto, o contexto é importante: otimize para as condições de dispositivo e rede dos usuários.

ALT_TEXT_HERE
O Google Analytics pode fornecer insights sobre as classes de dispositivos móveis que seus usuários reais estão usando para acessar seu site. Isso pode oferecer oportunidades para entender as restrições reais de CPU/GPU com que eles estão operando.

Estamos mesmo enviando JavaScript demais? Err, possibly :)

Usando o HTTP Archive (os 500 mil principais sites) para analisar o estado do JavaScript em dispositivos móveis, podemos ver que 50% dos sites levam mais de 14 segundos para se tornarem interativos. Esses sites gastam até 4 segundos apenas analisando e compilando JS.

ALT_TEXT_HERE

Considerando o tempo necessário para buscar e processar o JS e outros recursos, não é surpresa que os usuários tenham que esperar um pouco antes de sentir que as páginas estão prontas para uso. Com certeza podemos melhorar isso.

Remover o JavaScript não crítico das suas páginas pode reduzir os tempos de transmissão, a análise e a compilação que consomem muita CPU e a possível sobrecarga de memória. Isso também ajuda a tornar suas páginas interativas mais rapidamente.

Tempo de execução

Não são apenas a análise e a compilação que podem ter um custo. A execução de JavaScript (executar o código depois de analisado/compilado) é uma das operações que precisa acontecer na linha de execução principal. Tempos de execução longos também podem atrasar o momento em que um usuário pode interagir com seu site.

ALT_TEXT_HERE

Se o script for executado por mais de 50 ms, o tempo até a interatividade será atrasado pelo tempo total necessário para baixar, compilar e executar o JS — Alex Russell

Para resolver isso, o JavaScript se beneficia de estar em pequenos blocos para evitar o bloqueio da linha de execução principal. Verifique se é possível reduzir a quantidade de trabalho que está sendo realizada durante a execução.

Outros custos

O JavaScript pode afetar o desempenho da página de outras maneiras:

  • Memória. As páginas podem parecer instáveis ou pausar com frequência devido à GC (coleta de lixo). Quando um navegador recupera memória, a execução do JS é pausada. Portanto, um navegador que coleta lixo com frequência pode pausar a execução com mais frequência do que gostaríamos. Evite vazamentos de memória e pausas frequentes de GC para manter as páginas sem instabilidade.
  • Durante o tempo de execução, o JavaScript de longa duração pode bloquear a linha de execução principal, fazendo com que as páginas não respondam. Dividir o trabalho em partes menores (usando requestAnimationFrame() ou requestIdleCallback() para programação) pode minimizar problemas de responsividade, o que ajuda a melhorar a Interaction to Next Paint (INP).

Padrões para reduzir o custo de entrega do JavaScript

Quando você tenta manter os tempos de análise/compilação e transmissão de rede para JavaScript lentos, há padrões que podem ajudar, como a divisão em blocos com base em rotas ou PRPL.

PRPL

PRPL (Push, Render, Pré-armazenamento em cache, Lazy-load) é um padrão que otimiza a interatividade por meio de divisão de código e armazenamento em cache agressivos:

ALT_TEXT_HERE

Vamos visualizar o impacto que isso pode ter.

Analisamos o tempo de carregamento de sites para dispositivos móveis e apps Web progressivos usando as estatísticas de chamada de ambiente de execução do V8. Como podemos ver, o tempo de análise (mostrado em laranja) é uma parte significativa do tempo gasto por muitos desses sites:

ALT_TEXT_HERE

O Wego, um site que usa o PRPL, consegue manter um tempo de análise baixo para as rotas, tornando-se interativo muito rapidamente. Muitos dos outros sites acima adotaram a divisão de código e os montantes de desempenho para tentar reduzir os custos de JS.

Bootstrapping progressivo

Muitos sites otimizam a visibilidade do conteúdo em detrimento da interatividade. Para conseguir uma primeira exibição rápida quando você tem grandes pacotes de JavaScript, os desenvolvedores às vezes usam a renderização do lado do servidor e, em seguida, fazem um "upgrade" para anexar manipuladores de eventos quando o JavaScript é finalmente buscado.

Cuidado: isso tem custos próprios. Você 1) geralmente envia uma resposta HTML maior, que pode prejudicar a interatividade, e 2) pode deixar o usuário em um vale da estranheza, em que metade da experiência não pode ser interativa até que o JavaScript termine o processamento.

A inicialização progressiva pode ser uma abordagem melhor. Envie uma página minimamente funcional (composta apenas pelo HTML/JS/CSS necessário para a rota atual). À medida que mais recursos chegam, o app pode fazer o carregamento lento e desbloquear mais recursos.

ALT_TEXT_HERE
Progressive Bootstrapping, de Paul Lewis

Carregar o código proporcionalmente ao que está em exibição é o ideal. O PRPL e a inicialização progressiva são padrões que podem ajudar a fazer isso.

Conclusões

O tamanho da transmissão é fundamental para redes de baixa qualidade. O tempo de análise é importante para dispositivos vinculados à CPU. É importante manter esses valores baixos.

As equipes têm tido sucesso ao adotar montantes de desempenho rígidos para manter baixos os tempos de transmissão e análise/tempo de compilação do JavaScript. Consulte o artigo de Alex Russell "Can You Afford It?: Real-world Web Performance Budgets" para orientações sobre montantes para dispositivos móveis.

ALT_TEXT_HERE
É útil considerar quanto "espaço" de JS as decisões de arquitetura que tomamos podem deixar para a lógica do app.

Se você estiver criando um site para dispositivos móveis, faça o possível para desenvolver em hardware representativo, mantenha os tempos de análise/compilação do JavaScript baixos e adote um montante de desempenho para garantir que sua equipe possa ficar de olho nos custos do JavaScript.

Saiba mais