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 chamadasimport()
dinâmicas.chunks: initial
se refere a chamadasimport
estáticas.chunks: all
abrange importações dinâmicas deimport()
e estáticas, permitindo que você compartilhe blocos entre as importaçõesasync
einitial
.
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 blocoinitial
, que inclui o módulomain.js
e./my-function.js
. - O bloco
async
, que inclui apenasform-validation.js
(contendo um hash de arquivo no nome do recurso, se configurado). Esse bloco só será baixado se e quandocondition
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?
import()
.import
estático
Que tipo de instrução import
precisa estar na parte de cima
de um módulo JavaScript e em nenhum outro local?
import()
.import
estático
Ao usar SplitChunksPlugin
no webpack, qual é a
diferença entre um bloco async
e um
bloco initial
?
async
são carregados usando import()
e os blocos initial
são carregados usando
import
estático.
async
são carregados usando import
estáticos e os blocos initial
são carregados usando import()
dinâmico
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.