Avaliação de script e tarefas longas

Ao carregar scripts, o navegador leva algum tempo para avaliá-los antes da execução, o que pode levar a tarefas longas. Saiba como a avaliação de script funciona e o que fazer para evitar que ela gere tarefas longas durante o carregamento da página.

Quando se trata de otimizar a Interação com a próxima exibição (INP, na sigla em inglês), a maioria dos conselhos que você vai encontrar é que você mesmo otimize as interações. Por exemplo, no guia de otimização de tarefas longas, são discutidas técnicas como rendimento com setTimeout, isInputPending e assim por diante. Essas técnicas são benéficas, porque permitem que a linha de execução principal tenha um pouco mais de espaço, evitando tarefas longas, o que pode permitir mais oportunidades de interações e outras atividades serem executadas mais cedo, em vez de se eles tivessem que esperar por uma única tarefa longa.

Mas e as tarefas longas resultantes do carregamento de scripts em si? Essas tarefas podem interferir nas interações do usuário e afetar o INP da página durante o carregamento. Este guia mostra como os navegadores lidam com tarefas iniciadas pela avaliação do script e analisa o que você pode fazer para interromper o trabalho de avaliação do script para que sua linha de execução principal seja mais responsiva à entrada do usuário enquanto a página é carregada.

O que é avaliação de script?

Se você criou um perfil de um aplicativo que envia muito JavaScript, deve ter visto tarefas longas, em que o culpado é chamado de Avaliar script.

Trabalho de avaliação de script visto no Performance Profiler do Chrome DevTools. O trabalho gera uma tarefa longa durante a inicialização, o que bloqueia a capacidade da linha de execução principal de responder às interações do usuário.
Trabalho de avaliação do script, conforme mostrado no Performance Profiler no Chrome DevTools. Nesse caso, o trabalho é suficiente para causar uma tarefa longa que impede que a linha de execução principal assuma outro trabalho, incluindo tarefas que impulsionam as interações do usuário.

A avaliação do script é uma parte necessária da execução do JavaScript no navegador, já que o JavaScript é compilado just-in-time antes da execução. Quando um script é avaliado, primeiro ele é analisado em busca de erros. Se o analisador não encontrar erros, o script será compilado em bytecode e poderá continuar na execução.

Embora seja necessário, a avaliação do script pode ser problemática, porque os usuários podem tentar interagir com uma página logo após ela ser renderizada. No entanto, o fato de uma página ser renderizada não significa que ela terminou de ser carregada. As interações que ocorrem durante o carregamento podem ser atrasadas porque a página está ocupada avaliando scripts. Não há garantia de que a interação desejada possa ocorrer nesse momento (como um script responsável por ela ainda não foi carregado), mas pode haver interações dependentes de JavaScript que estão prontas ou a interatividade não depende do JavaScript.

A relação entre os scripts e as tarefas que os avaliam

A forma como as tarefas responsáveis pela avaliação do script são iniciadas depende se o script que você está carregando é carregado por um elemento <script> normal ou se um script é um módulo carregado com a type=module. Como os navegadores tendem a lidar com as coisas de modo diferente, a maneira como os principais mecanismos de navegador lidam com a avaliação de scripts será considerada em que o comportamento da avaliação entre eles varia.

Como carregar scripts com o elemento <script>

O número de tarefas enviadas para avaliar scripts geralmente tem uma relação direta com o número de elementos <script> em uma página. Cada elemento <script> inicia uma tarefa para avaliar o script solicitado para que ele possa ser analisado, compilado e executado. Esse é o caso dos navegadores baseados no Chromium, do Safari e do Firefox.

Por que isso é importante? Digamos que você use um bundler para gerenciar scripts de produção e o configurou para agrupar tudo o que sua página precisa para ser executada em um único script. Se esse for o caso do seu site, uma única tarefa será enviada para avaliar esse script. Isso é ruim? Não necessariamente, a menos que o script seja grande.

É possível interromper o trabalho de avaliação do script evitando o carregamento de grandes blocos de JavaScript e carregar scripts menores e mais individuais usando outros elementos <script>.

Embora você deva sempre se esforçar para carregar o mínimo de JavaScript possível durante o carregamento da página, dividir seus scripts garante que, em vez de uma tarefa grande que possa bloquear a linha de execução principal, você tenha um número maior de tarefas menores que não bloquearão a linha de execução principal, ou pelo menos menos do que você começou.

Várias tarefas que envolvem avaliação de script, conforme visualizado no criador de perfil de desempenho do Chrome DevTools. Como vários scripts menores são carregados em vez de menos scripts maiores, as tarefas têm menos probabilidade de se tornarem tarefas longas. Isso permite que a linha de execução principal responda à entrada do usuário mais rapidamente.
Várias tarefas geradas para avaliar scripts como resultado de vários elementos <script> presentes no HTML da página. É melhor fazer isso do que enviar um grande pacote de scripts aos usuários, porque a probabilidade de bloquear a linha de execução principal é maior.

A divisão de tarefas para avaliação de script é semelhante ao resultado durante callbacks de evento executados durante uma interação. No entanto, com a avaliação do script, o mecanismo de rendimento divide o JavaScript carregado em vários scripts menores, em vez de um número menor de scripts maiores do que a probabilidade de bloquear a linha de execução principal.

Carregar scripts com o elemento <script> e o atributo type=module

Agora é possível carregar módulos ES de forma nativa no navegador com o atributo type=module no elemento <script>. Essa abordagem de carregamento de script traz alguns benefícios para a experiência do desenvolvedor, como não ter que transformar o código para uso em produção, especialmente quando usada em combinação com a importação de mapas. No entanto, carregar scripts dessa maneira agenda tarefas que diferem de navegador para navegador.

Navegadores baseados no Chromium

Em navegadores como o Chrome ou aqueles derivados dele, o carregamento de módulos ES usando o atributo type=module produz tipos diferentes de tarefas do que você normalmente veria quando não usa type=module. Por exemplo, será executada uma tarefa para cada script de módulo que envolva uma atividade identificada como Compile module.

Trabalho de compilação de módulos em várias tarefas, conforme visualizado no Chrome DevTools.
Comportamento de carregamento do módulo em navegadores baseados no Chromium. Cada script de módulo vai gerar uma chamada Compile module para compilar o conteúdo antes da avaliação.

Depois que os módulos forem compilados, qualquer código que for executado neles vai iniciar a atividade rotulada como Avaliar módulo.

Avaliação just-in-time de um módulo, conforme visualizado no painel de desempenho do Chrome DevTools.
Quando o código em um módulo é executado, esse módulo é avaliado no momento certo.

O efeito aqui (pelo menos no Chrome e em navegadores relacionados) é que as etapas de compilação são interrompidas ao usar módulos ES. Essa é uma vitória clara no que diz respeito ao gerenciamento de tarefas longas. No entanto, o trabalho de avaliação do módulo resultante que resulta ainda significa que você está incorrendo em custos inevitáveis. Embora você deva fornecer o mínimo de JavaScript possível, o uso de módulos ES, independentemente do navegador, oferece os seguintes benefícios:

  • Todo o código do módulo é executado automaticamente no modo estrito, o que permite possíveis otimizações por mecanismos JavaScript que não poderiam ser feitas em um contexto não restrito.
  • Os scripts carregados usando type=module são tratados como se tivessem sido adiados por padrão. É possível usar o atributo async em scripts carregados com type=module para mudar esse comportamento.

Safari e Firefox

Quando os módulos são carregados no Safari e no Firefox, cada um deles é avaliado em uma tarefa separada. Teoricamente, isso significa que é possível carregar um único módulo de nível superior que consiste apenas em instruções estáticas import para outros módulos, e cada módulo carregado vai gerar uma solicitação de rede e uma tarefa separadas para avaliá-lo.

Carregando scripts com o import() dinâmico

import() dinâmico é outro método para carregar scripts. Ao contrário das instruções import estáticas que precisam estar na parte superior de um módulo ES, uma chamada import() dinâmica pode aparecer em qualquer lugar de um script para carregar um bloco de JavaScript sob demanda. Essa técnica é chamada de divisão de código.

A import() dinâmica tem duas vantagens quando se trata de melhorar o INP:

  1. Os módulos que são adiados para carregar reduzem a contenção de encadeamento principal durante a inicialização, reduzindo a quantidade de JavaScript carregada naquele momento. Isso libera a linha de execução principal para que ela possa ser mais responsiva às interações do usuário.
  2. Quando chamadas import() dinâmicas são feitas, cada chamada separa efetivamente a compilação e a avaliação de cada módulo para a própria tarefa. É claro que uma import() dinâmica que carrega um módulo muito grande inicia uma tarefa de avaliação de script bastante grande, e isso pode interferir na capacidade da linha de execução principal de responder à entrada do usuário se a interação ocorrer ao mesmo tempo que a chamada import() dinâmica. Portanto, ainda é muito importante carregar o mínimo de JavaScript possível.

As chamadas dinâmicas de import() se comportam de maneira semelhante em todos os principais mecanismos de navegador: as tarefas de avaliação de script resultantes serão as mesmas que a quantidade de módulos importados dinamicamente.

Como carregar scripts em um worker da Web

Os Web workers são um caso de uso especial do JavaScript. Os Web workers são registrados na linha de execução principal e o código dentro deles é executado na própria linha de execução. Isso é muito benéfico porque, embora o código que registra o web worker seja executado na linha de execução principal, o código dentro dele não. Isso reduz o congestionamento da linha de execução principal e pode ajudar a manter a linha de execução principal mais responsiva às interações do usuário.

Além de reduzir o trabalho da linha de execução principal, os web workers eles podem carregar scripts externos para serem usados no contexto do worker, seja por instruções importScripts ou import estáticas em navegadores compatíveis com workers de módulo. O resultado é que qualquer script solicitado por um web worker é avaliado fora da linha de execução principal.

Vantagens, desvantagens e considerações

Dividir seus scripts em arquivos menores e separados ajuda a limitar tarefas longas em vez de carregar menos arquivos muito maiores. No entanto, é importante considerar alguns aspectos ao decidir como dividir os scripts.

Eficiência da compressão

A compactação é um fator quando se trata de dividir scripts. Quando os scripts são menores, a compactação fica um pouco menos eficiente. A compactação será muito melhor para scripts maiores. Embora aumentar a eficiência de compactação ajude a manter os tempos de carregamento dos scripts os mais baixos possíveis, é preciso fazer um equilíbrio para garantir que você divida os scripts em pedaços menores suficientes para facilitar a melhor interatividade durante a inicialização.

Os bundlers são ferramentas ideais para gerenciar o tamanho da saída dos scripts dos quais o site depende:

  • Em relação ao webpack, o plug-in SplitChunksPlugin pode ajudar. Consulte a documentação de SplitChunksPlugin (link em inglês) para ver as opções que você pode configurar para gerenciar os tamanhos dos recursos.
  • Para outros bundlers, como Rollup e esbuild, é possível gerenciar tamanhos de arquivos de script usando chamadas import() dinâmicas no código. Esses bundlers, assim como o webpack, dividem automaticamente o recurso importado dinamicamente em seu próprio arquivo, evitando tamanhos de pacote iniciais maiores.

Invalidação de cache

A invalidação de cache desempenha um papel importante na velocidade de carregamento de uma página em visitas repetidas. Quando você envia pacotes grandes e monolíticos de script, fica em desvantagem com o processo de cache do navegador. Isso ocorre porque, quando você atualiza seu código próprio, seja com a atualização de pacotes ou de correções de bugs no envio, o pacote inteiro se torna invalidado e precisa ser transferido novamente.

Ao dividir seus scripts, você não está apenas dividindo o trabalho de avaliação de script em tarefas menores, mas também aumenta a probabilidade de os visitantes que retornam obterem mais scripts no cache do navegador em vez de na rede. Isso se traduz em um carregamento de página geral mais rápido.

Módulos aninhados e desempenho de carregamento

Se você estiver enviando módulos ES em produção e carregando-os com o atributo type=module, vai precisar saber como o aninhamento de módulos pode afetar o tempo de inicialização. O aninhamento de módulo se refere a quando um módulo ES importa estaticamente outro módulo ES que importa estaticamente outro módulo ES:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Se os módulos ES não estiverem agrupados, o código anterior vai resultar em uma cadeia de solicitações de rede: quando a.js for solicitado de um elemento <script>, outra solicitação de rede será enviada para b.js, que envolve outra solicitação para c.js. Uma maneira de evitar isso é usar um bundler, mas configure o bundler para dividir os scripts a fim de distribuir o trabalho de avaliação deles.

Se você não quiser usar um bundler, outra maneira de contornar chamadas de módulos aninhados é usar a dica de recurso modulepreload (link em inglês), que pré-carrega os módulos ES com antecedência para evitar cadeias de solicitações de rede.

Conclusão

Otimizar a avaliação de scripts no navegador é, sem dúvida, uma tarefa complicada. A abordagem depende dos requisitos e das restrições do seu site. No entanto, ao dividir os scripts, você está distribuindo o trabalho de avaliação do script em várias tarefas menores. Isso permite que a linha de execução principal processe as interações do usuário com mais eficiência, em vez de bloquear a linha de execução principal.

Para recapitular, aqui estão algumas coisas que você pode fazer para dividir grandes tarefas de avaliação de script:

  • Ao carregar scripts usando o elemento <script> sem o atributo type=module, evite carregar scripts muito grandes, porque eles vão iniciar tarefas de avaliação de script que usam muitos recursos e bloqueiam a linha de execução principal. Distribuir seus scripts por mais elementos <script> para dividir esse trabalho.
  • Usar o atributo type=module para carregar módulos ES de maneira nativa no navegador iniciará tarefas individuais para avaliação para cada script de módulo separado.
  • Reduza o tamanho dos pacotes iniciais usando chamadas import() dinâmicas. Isso também funciona em bundlers, porque eles tratam cada módulo importado dinamicamente como um "ponto de divisão", resultando na geração de um script separado para cada módulo importado dinamicamente.
  • Lembre-se de ponderar as compensações como a eficiência da compactação e a invalidação de cache. Scripts maiores compactam melhor, mas são mais propensos a envolver um trabalho de avaliação de script mais caro em menos tarefas e resultar na invalidação do cache do navegador, levando a uma menor eficiência geral de armazenamento em cache.
  • Se estiver usando módulos ES nativamente sem agrupamento, use a dica de recurso modulepreload para otimizar o carregamento deles durante a inicialização.
  • Como sempre, forneça a menor quantidade possível de JavaScript.

É um equilíbrio, mas ao dividir os scripts e reduzir os payloads iniciais usando o import() dinâmico, é possível alcançar um melhor desempenho de inicialização e acomodar melhor as interações do usuário durante esse período crucial de inicialização. Isso vai ajudar você a pontuar melhor na métrica de INP, proporcionando uma melhor experiência do usuário.

Imagem principal do Unsplash, por Markus Spiske.