JavaScript de divisão de código

O carregamento de recursos JavaScript grandes afeta significativamente a velocidade da página. Dividir o JavaScript em partes menores e baixar apenas o que é necessário para uma página funcionar durante a inicialização pode melhorar muito a capacidade de resposta ao carregamento da página, o que, por sua vez, pode melhorar a Interação com a próxima renderização (INP).

À medida que uma página baixa, analisa e compila arquivos JavaScript grandes, ela pode ficar sem resposta por períodos de tempo. Os elementos da página ficam visíveis porque fazem parte do HTML inicial de uma página e são estilizados por CSS. No entanto, como o JavaScript necessário para ativar esses elementos interativos, bem como outros scripts carregados pela página, pode estar analisando e executando o JavaScript para que eles funcionem. O resultado é que o usuário pode sentir que a interação foi significativamente atrasada ou até mesmo interrompida.

Isso geralmente acontece porque a linha de execução principal está bloqueada, já que o JavaScript é analisado e compilado nela. Se esse processo demorar muito, os elementos interativos da página podem não responder rápido o suficiente à entrada do usuário. Uma solução para isso é carregar apenas o JavaScript necessário para o funcionamento da página, adiando o carregamento de outros JavaScripts para mais tarde usando uma técnica conhecida como divisão de código. Este módulo se concentra na última dessas duas técnicas.

Reduza a análise e a execução de JavaScript durante a inicialização com a divisão de código

O Lighthouse mostra um aviso quando a execução do JavaScript leva mais de 2 segundos e falha quando leva mais de 3, 5 segundos. O parsing e a execução excessivos de JavaScript são um problema potencial em qualquer ponto do ciclo de vida da página, já que podem aumentar o atraso de entrada de uma interação se o momento em que o usuário interage com a página coincidir com o momento em que as tarefas da linha de execução principal responsáveis por processar e executar JavaScript estão em execução.

Além disso, a execução e a análise excessivas de JavaScript são particularmente problemáticas durante o carregamento inicial da página, já que esse é o ponto do ciclo de vida em que os usuários provavelmente vão interagir com ela. Na verdade, o Tempo total de bloqueio (TBT), uma métrica de capacidade de resposta ao carregamento, tem uma alta correlação com o INP, o que sugere que os usuários têm uma alta tendência a tentar interações durante o carregamento inicial da página.

A auditoria do Lighthouse que informa o tempo gasto na execução de cada arquivo JavaScript solicitado pela página é útil porque ajuda a identificar exatamente quais scripts podem ser candidatos à divisão de código. Você pode ir mais longe usando a ferramenta de cobertura no Chrome DevTools para identificar exatamente quais partes do JavaScript de uma página não são usadas durante o carregamento.

A divisão de código é uma técnica útil que pode reduzir os payloads iniciais de JavaScript de uma página. Ele permite dividir um pacote JavaScript em duas partes:

  • O JavaScript necessário no carregamento da página e, portanto, não pode ser carregado em nenhum outro momento.
  • O restante do JavaScript pode ser carregado mais tarde, geralmente no momento em que o usuário interage com um determinado elemento interativo na página.

A divisão de código pode ser feita usando a sintaxe dynamic import(). Essa sintaxe, ao contrário dos elementos <script>, que solicita um determinado recurso JavaScript durante a inicialização, faz uma solicitação de um recurso JavaScript em um momento posterior durante o ciclo de vida da página.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

No snippet de JavaScript anterior, o módulo validate-form.mjs é baixado, analisado e executado somente quando um usuário desfoca qualquer um dos campos <input> de um formulário. Nessa situação, o recurso JavaScript responsável por acionar a lógica de validação do formulário só é envolvido com a página quando ela tem mais chances de ser usada.

Bundlers de JavaScript, como webpack, Parcel, Rollup e esbuild, podem ser configurados para dividir pacotes de JavaScript em blocos menores sempre que encontrarem uma chamada dinâmica import() no código-fonte. A maioria dessas ferramentas faz isso automaticamente, mas o esbuild, em particular, exige que você ative essa otimização.

Observações úteis sobre divisão de código

Embora a divisão de código seja um método eficaz para reduzir a disputa da linha de execução principal durante o carregamento inicial da página, é importante ter algumas coisas em mente se você decidir auditar seu código-fonte JavaScript para oportunidades de divisão de código.

Use um bundler, se possível

É uma prática comum para os desenvolvedores usar módulos JavaScript durante o processo de desenvolvimento. É uma excelente melhoria na experiência do desenvolvedor que melhora a legibilidade e a capacidade de manutenção do código. No entanto, há algumas características de desempenho abaixo do ideal que podem resultar do envio de módulos JavaScript para produção.

O mais importante é usar um bundler para processar e otimizar o código-fonte, incluindo os módulos que você pretende dividir. Os bundlers são muito eficazes não apenas na aplicação de otimizações ao código-fonte JavaScript, mas também no equilíbrio de considerações de desempenho, como tamanho do pacote e taxa de compressão. A eficácia da compressão aumenta com o tamanho do pacote, mas os agrupadores também tentam garantir que os pacotes não sejam tão grandes a ponto de gerar tarefas longas devido à avaliação de script.

Os agrupadores também evitam o problema de enviar um grande número de módulos não agrupados pela rede. Arquiteturas que usam módulos JavaScript costumam ter árvores de módulos grandes e complexas. Quando as árvores de módulos não são agrupadas, cada módulo representa uma solicitação HTTP separada, e a interatividade no seu web app pode ser atrasada se você não agrupar os módulos. Embora seja possível usar a dica de recurso <link rel="modulepreload"> para carregar grandes árvores de módulos o mais cedo possível, os pacotes JavaScript ainda são preferíveis do ponto de vista do desempenho de carregamento.

Não desative a compilação de streaming sem querer

O mecanismo JavaScript V8 do Chromium oferece várias otimizações prontas para uso para garantir que o código JavaScript de produção seja carregado da maneira mais eficiente possível. Uma dessas otimizações é conhecida como compilação de streaming, que, assim como a análise incremental de HTML transmitida para o navegador, compila partes transmitidas de JavaScript à medida que chegam da rede.

Há algumas maneiras de garantir que a compilação de streaming ocorra para seu aplicativo da Web no Chromium:

  • Transforme seu código de produção para evitar o uso de módulos JavaScript. Os bundlers podem transformar seu código-fonte JavaScript com base em um destino de compilação, e o destino geralmente é específico para um determinado ambiente. O V8 vai aplicar a compilação de streaming a qualquer código JavaScript que não use módulos, e você pode configurar o bundler para transformar seu código de módulo JavaScript em uma sintaxe que não use módulos JavaScript e recursos deles.
  • Se você quiser enviar módulos JavaScript para produção, use a extensão .mjs. Se o JavaScript de produção usa módulos ou não, não há um tipo de conteúdo especial para JavaScript que usa módulos em comparação com JavaScript que não usa. No V8, você desativa a compilação de streaming quando envia módulos JavaScript em produção usando a extensão .js. Se você usar a extensão .mjs para módulos JavaScript, o V8 poderá garantir que a compilação de streaming para código JavaScript baseado em módulos não seja interrompida.

Não deixe que essas considerações o dissuadam de usar a divisão de código. A divisão de código é uma maneira eficaz de reduzir os payloads JavaScript iniciais para os usuários, mas, ao usar um bundler e saber como preservar o comportamento de compilação de streaming do V8, é possível garantir que o código JavaScript de produção seja o mais rápido possível para os usuários.

Demonstração de importação dinâmica

webpack

O webpack vem com um plug-in chamado SplitChunksPlugin, que permite configurar como o bundler divide os arquivos JavaScript. O webpack reconhece as instruções dinâmicas import() e estáticas import. O comportamento de SplitChunksPlugin pode ser modificado especificando a opção chunks na configuração:

  • chunks: async é o valor padrão e se refere a chamadas dinâmicas de import().
  • chunks: initial se refere a chamadas estáticas de import.
  • O chunks: all abrange importações dinâmicas import() e estáticas, permitindo que você compartilhe partes entre importações async e initial.

Por padrão, sempre que o webpack encontra uma instrução import() dinâmica, ele cria um trecho separado para esse módulo:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

A configuração padrão do webpack para o snippet de código anterior resulta em dois pedaços separados:

  • O trecho main.js, que o webpack classifica como um trecho initial, que inclui o módulo main.js e ./my-function.js.
  • O bloco async, que inclui apenas form-validation.js (contendo um hash de arquivo no nome do recurso, se configurado). Esse trecho só será baixado se e quando condition for verdadeiro.

Essa configuração permite adiar o carregamento do bloco form-validation.js até que ele seja realmente necessário. Isso pode melhorar a capacidade de resposta do carregamento, reduzindo o tempo de avaliação de script durante o carregamento inicial da página. O download e a avaliação de script para o bloco form-validation.js ocorrem quando uma condição especificada é atendida, em que caso, o módulo importado dinamicamente é baixado. Um exemplo pode ser uma condição em que um polyfill é baixado apenas para um navegador específico ou, como no exemplo anterior, o módulo importado é necessário para uma interação do usuário.

Por outro lado, mudar a configuração SplitChunksPlugin para especificar chunks: initial garante que o código seja dividido apenas em partes iniciais. Esses são pedaços como os importados estaticamente ou listados na propriedade entry do webpack. No exemplo anterior, o bloco resultante seria uma combinação de form-validation.js e main.js em um único arquivo de script, resultando em um desempenho de carregamento inicial da página potencialmente pior.

As opções de SplitChunksPlugin também podem ser configuradas para separar scripts maiores em vários menores. Por exemplo, usando a opção maxSize para instruir o webpack a dividir os blocos em arquivos separados se eles excederem o que é especificado por maxSize. Dividir arquivos de script grandes em arquivos menores pode melhorar a capacidade de resposta do carregamento, já que, em alguns casos, o trabalho de avaliação de script com uso intensivo da CPU é dividido em tarefas menores, que têm menos probabilidade de bloquear a linha de execução principal por períodos mais longos.

Além disso, gerar arquivos JavaScript maiores também significa que os scripts têm mais chances de sofrer invalidação de cache. Por exemplo, se você enviar um script muito grande com o framework e o código do aplicativo próprio, todo o pacote poderá ser invalidado se apenas o framework for atualizado, mas nada mais no recurso agrupado.

Por outro lado, arquivos de script menores aumentam a probabilidade de um visitante recorrente recuperar recursos do cache, resultando em carregamentos de página mais rápidos em visitas repetidas. No entanto, arquivos menores se beneficiam menos da compactação do que os maiores e podem aumentar o tempo de ida e volta da rede em carregamentos de página com um cache do navegador não inicializado. É preciso encontrar um equilíbrio entre a eficiência do cache, a eficácia da compactação e o tempo de avaliação do script.

Demonstração do webpack

webpack SplitChunksPlugin demo.

Teste seus conhecimentos

Qual tipo de instrução import é usada ao realizar a divisão de código?

Dinâmica import().
Correto.
Estático import.
Tente novamente.

Qual tipo de instrução import precisa estar na parte de cima de um módulo JavaScript e em nenhum outro local?

Dinâmica import().
Tente novamente.
Estático import.
Correto.

Ao usar SplitChunksPlugin no webpack, qual é a diferença entre um trecho async e um trecho initial?

Os blocos async são carregados usando import() dinâmico e os blocos initial são carregados usando import estático.
Correto.
Os blocos async são carregados usando import estático e os blocos initial são carregados usando import() dinâmico.
Tente novamente.

Próximo: imagens com carregamento lento e elementos <iframe>

Embora seja um tipo de recurso caro, o JavaScript não é o único que pode ter o carregamento adiado. Os elementos de imagem e <iframe> são recursos potencialmente caros por si só. Assim como no JavaScript, você pode adiar o carregamento de imagens e do elemento <iframe> usando o carregamento lento, que será explicado no próximo módulo deste curso.