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 fazer o download apenas do que é necessário para que a página funcione durante a inicialização pode melhorar muito a capacidade de resposta do carregamento da página, o que, por sua vez, pode melhorar a Interação com a Next Paint (INP).

À medida que uma página faz o download, analisa e compila arquivos JavaScript grandes, ela pode não responder por períodos. Os elementos da página são visíveis porque fazem parte do HTML inicial e estilizados por CSS. No entanto, como o JavaScript necessário para acionar esses elementos interativos, assim 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 como se a interação tivesse sido significativamente atrasada ou até mesmo completamente interrompida.

Isso geralmente acontece porque a linha de execução principal é bloqueada, já que o JavaScript é analisado e compilado na linha de execução principal. Se esse processo demorar muito, talvez os elementos interativos da página não respondam com rapidez suficiente à entrada do usuário. Uma solução para isso é carregar apenas o JavaScript necessário para a página funcionar e adiar o carregamento de outro JavaScript usando uma técnica conhecida como divisão de código. O foco deste módulo é a segunda técnica.

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

O Lighthouse gera um aviso quando a execução do JavaScript leva mais de dois segundos e falha quando leva mais de 3, 5 segundos. O excesso de análise e execução de JavaScript é um problema potencial em qualquer ponto do ciclo de vida da página, já que pode aumentar o atraso de entrada de uma interação quando o momento em que o usuário interage com a página coincide com o momento em que as principais tarefas da linha de execução responsáveis pelo processamento e execução do JavaScript estão em execução.

Além disso, o excesso de execução e análise de JavaScript é particularmente problemático durante o carregamento inicial da página, porque é o ponto no ciclo de vida da página em que os usuários estão bastante propensos a interagir com ela. Na verdade, o Tempo total de bloqueio (TBT, na sigla em inglês), uma métrica de capacidade de resposta de carga, está altamente correlacionado com o INP, sugerindo que os usuários têm uma alta tendência de 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 para ajudar a identificar exatamente quais scripts podem ser candidatos à divisão de código. Para ir além, use 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 em JavaScript de uma página. Ele permite dividir um pacote do JavaScript em duas partes:

  • O JavaScript é necessário no carregamento da página e, portanto, não pode ser carregado em nenhum outro momento.
  • JavaScript restante que pode ser carregado posteriormente, na maioria das vezes 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 dinâmica import(). Essa sintaxe, diferente dos elementos <script>, que solicitam um determinado recurso JavaScript durante a inicialização, faz uma solicitação de um recurso JavaScript posteriormente 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 JavaScript anterior, o módulo validate-form.mjs é transferido por download, 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 direcionar a lógica de validação do formulário só está envolvido com a página quando é mais provável que ela seja realmente usada.

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

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

Embora a divisão de código seja um método eficaz de reduzir a contenção da linha de execução principal durante o carregamento inicial da página, é importante lembrar de alguns detalhes se você decidir auditar o código-fonte JavaScript em busca de oportunidades de divisão de código.

Use um bundler se puder

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

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

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

Não desativar acidentalmente a compilação em streaming

O mecanismo JavaScript V8 do Chromium oferece várias otimizações prontas 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, como a análise incremental de HTML transmitido para o navegador, compila blocos transmitidos 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:

  • Transformar o código de produção para evitar o uso de módulos JavaScript. Os bundlers podem transformar o código-fonte JavaScript com base em um destino de compilação, e o destino geralmente é específico de um determinado ambiente. O V8 aplicará a compilação de streaming a qualquer código JavaScript que não use módulos, e você poderá configurar seu bundler para transformar o código do módulo JavaScript em uma sintaxe que não use módulos JavaScript e os recursos relacionados.
  • Se você quiser enviar módulos JavaScript para produção, use a extensão .mjs. Mesmo que o JavaScript de produção não use módulos, não há um tipo de conteúdo especial para JavaScript que usa módulos, enquanto JavaScript que não usa. Quanto ao V8, você desativa a compilação de streaming ao enviar 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ódulo não seja corrompida.

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

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

Pacote da web

O webpack vem com um plug-in chamado SplitChunksPlugin, que permite configurar como o bundler divide arquivos JavaScript. O webpack reconhece as instruções import() dinâmicas e import estáticas. 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 import() dinâmicas.
  • chunks: initial se refere a chamadas import estáticas.
  • chunks: all abrange importações dinâmicas de import() e estáticas, permitindo que você compartilhe blocos entre as importações async e initial.

Por padrão, sempre que o webpack encontra uma instrução import() dinâmica, ele cria um bloco 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 blocos separados:

  • O bloco main.js, que o webpack é classificado como um bloco 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 bloco só será baixado se e quando condition for verdadeira (link em inglês).

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, reduzindo o tempo de avaliação do script durante o carregamento inicial da página. O download e a avaliação de scripts para o bloco form-validation.js ocorrem quando uma condição especificada é atendida. Nesse caso, é feito o download do módulo importado dinamicamente. Um exemplo pode ser uma condição em que um polyfill é transferido por download 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 blocos iniciais. Eles são blocos como os importados estaticamente ou listados na propriedade entry (link em inglês) do webpack. Analisando o 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 de página inicial 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 blocos em arquivos separados caso eles excedam o que é especificado por maxSize. A divisão de arquivos de script grandes em arquivos menores pode melhorar a capacidade de resposta da carga, já que, em alguns casos, o trabalho de avaliação de scripts com uso intenso 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 são mais propensos a 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 empacotado.

Por outro lado, arquivos de script menores aumentam a probabilidade de que um visitante recorrente recupere 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 retorno da rede nos carregamentos de página com um cache de navegador sem preparação. É preciso ter cuidado para encontrar o equilíbrio entre a eficiência do armazenamento em cache, a eficácia da compactação e o tempo de avaliação do script.

demonstração do webpack

Demonstração do SplitChunksPlugin do webpack (em inglês).

teste seus conhecimentos

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

Dinâmico import().
Correto.
import estático
Tente novamente.

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

Dinâmico import().
Tente novamente.
import estático
Correto.

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

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

A seguir: imagens de carregamento lento e elementos <iframe>

Embora esse tipo de recurso tende a ser bastante caro, o JavaScript não é o único tipo de recurso que pode adiar o carregamento. Por si só, os elementos de imagem e <iframe> são recursos caros. Assim como no JavaScript, é possível adiar o carregamento de imagens e do elemento <iframe> com o carregamento lento, que é explicado no próximo módulo deste curso.