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 causar 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 Interaction to Next Paint (INP), a maioria do conselho que você vai encontrar é otimizar as interações por conta própria. Por exemplo, no guia de otimização de tarefas longas, você verá técnicas como a geração de resultados com setTimeout, entre outras. Essas técnicas são benéficas, porque permitem que a linha de execução principal tenha mais espaço, evitando tarefas longas, o que pode permitir que mais interações e outras atividades sejam executadas mais cedo do que se tivessem que esperar por uma única tarefa longa.

No entanto, e as tarefas longas provenientes do carregamento de scripts propriamente dito? Essas tarefas podem interferir nas interações do usuário e afetar o INP de uma página durante o carregamento. Este guia vai explorar como os navegadores lidam com as tarefas iniciadas pela avaliação de script e ver o que é possível fazer para interromper o trabalho de avaliação de script para que a linha de execução principal seja mais responsiva à entrada do usuário durante o carregamento da página.

O que é a avaliação de script?

Se você criou o perfil de um aplicativo que envia muitos JavaScript, pode ter visto tarefas longas, em que o culpado é rotulado Avaliar Script.

A avaliação do script funciona conforme mostrado no gerador de perfis de desempenho do Chrome DevTools. O trabalho causa 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.
A avaliação de script funciona conforme mostrado no criador de perfil de desempenho 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 de script é uma parte necessária da execução de JavaScript no navegador, já que o JavaScript é compilado no momento certo 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, em seguida, poderá continuar em execução.

Embora necessária, a avaliação de script pode ser problemática, já que os usuários podem tentar interagir com uma página logo após a renderização inicial. No entanto, o fato de uma página ter sido renderizada não significa que ela terminou de ser carregada. As interações que ocorrem durante o carregamento podem sofrer atrasos porque a página está ocupada avaliando scripts. Embora não haja garantia de que uma interação possa ocorrer nesse momento (já que um script responsável por ela pode ainda não ter sido carregado), é possível que haja interações dependentes de JavaScript que estejam prontas ou a interatividade não dependa dele de jeito nenhum.

a relação entre scripts e 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 com um elemento <script> típico ou se o script é um módulo carregado com o type=module. Como os navegadores tendem a lidar com as coisas de maneira diferente, como os principais mecanismos do navegador lidam com a avaliação de script será levado em consideração onde os comportamentos de avaliação de script variam entre eles.

Scripts carregados 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ê esteja usando um bundler para gerenciar scripts de produção e o tenha configurado para agrupar tudo que sua página precisa para ser executada em um único script. Se esse for o caso do seu site, haverá uma única tarefa enviada para avaliar esse script. Isso é algo ruim? Não necessariamente, a menos que o script seja enorme.

É possível interromper o trabalho de avaliação do script evitando o carregamento de grandes blocos de JavaScript e carregar scripts mais individuais e menores 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 os 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 envolvendo a avaliação de scripts conforme mostrado 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 longas, permitindo 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. É preferível enviar um pacote grande de scripts para os usuários, porque é mais provável que ele bloqueie a linha de execução principal.

Pense na divisão de tarefas para avaliação de script como algo semelhante ao resultado durante callbacks de eventos executados durante uma interação. No entanto, com a avaliação de script, o mecanismo de produção divide o JavaScript carregado em vários scripts menores, em vez de um número menor de scripts maiores do que os que têm mais chances de bloquear a linha de execução principal.

Scripts carregados 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 para o carregamento de scripts traz alguns benefícios para a experiência do desenvolvedor, como não ter que transformar o código para uso na produção, especialmente quando usada junto com mapas de importação. No entanto, carregar scripts dessa forma 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 de tarefas diferentes do que você normalmente veria quando não usa type=module. Por exemplo, uma tarefa para cada script de módulo que envolve uma atividade rotulada como Compile module é executada.

A compilação de módulos funciona em várias tarefas, conforme mostrado no Chrome DevTools.
Comportamento de carregamento de módulos em navegadores baseados no Chromium. Cada script de módulo gera 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 posteriormente neles iniciará a atividade rotulada como evaluate module.

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, ele é avaliado no momento certo.

O efeito aqui (pelo menos no Chrome e em navegadores relacionados) é que as etapas de compilação são divididas ao usar módulos ES. Essa é uma vitória clara em termos de gerenciamento de tarefas longas. No entanto, o trabalho de avaliação do módulo resultante ainda significa que você está gerando custos inevitáveis. Você deve se esforçar para fornecer o mínimo de JavaScript possível, mas 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. Isso significa que, teoricamente, é possível carregar um único módulo de nível superior composto apenas de 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.

Scripts carregados com o import() dinâmico

import() dinâmico é outro método de carregamento de scripts. Ao contrário das instruções import estáticas, que precisam estar na parte de cima de um módulo ES, uma chamada import() dinâmica pode aparecer em qualquer lugar do 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 adiados para carregamento posterior reduzem a contenção da linha de execução principal durante a inicialização, reduzindo a quantidade de JavaScript carregado 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 uma tarefa própria. É claro que uma import() dinâmica que carrega um módulo muito grande inicia uma tarefa de avaliação de script muito grande. 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. Por isso, ainda é muito importante carregar a menor quantidade possível de JavaScript.

As chamadas import() dinâmicas 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.

Scripts carregados em um worker da Web

Os workers da Web são um caso de uso especial de JavaScript. Os workers da Web são registrados na linha de execução principal e o código no worker é executado na própria linha de execução. Isso é muito benéfico porque o código que registra o web worker é executado na linha de execução principal, mas o código no web worker 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 próprios Web workers podem carregar scripts externos para serem usados no contexto do worker, seja por meio de instruções importScripts ou de instruções import estáticas em navegadores que oferecem suporte a workers do 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 ajuda a limitar tarefas longas em vez de carregar arquivos muito maiores. No entanto, é importante considerar alguns fatores ao decidir como dividir os scripts.

Eficiência da compactação

A compactação é um fator quando se trata de dividir scripts. Quando os scripts são menores, a compactação se torna um pouco menos eficiente. Scripts maiores se beneficiarão muito mais com a compactação. Embora aumentar a eficiência da compactação ajude a manter os tempos de carregamento dos scripts o mais baixo possível, é uma espécie de ato de equilíbrio para garantir que você esteja dividindo os scripts em partes menores suficientes para facilitar uma melhor interatividade durante a inicialização.

Os Bundlers são ferramentas ideais para gerenciar o tamanho da saída dos scripts de que seu site depende:

  • No que diz respeito ao webpack, o plug-in SplitChunksPlugin dele pode ajudar. Consulte a documentação do SplitChunksPlugin para ver opções que podem ser definidas para ajudar a gerenciar os tamanhos dos recursos.
  • Para outros bundlers, como Rollup e esbuild, é possível gerenciar os tamanhos dos arquivos de script usando chamadas dinâmicas import() no código. Esses bundlers, assim como o webpack, dividem automaticamente o recurso importado dinamicamente em seu próprio arquivo, evitando assim tamanhos maiores de pacotes iniciais.

Invalidação de cache

A invalidação de cache tem um papel importante na velocidade de carregamento de uma página em visitas repetidas. Ao enviar pacotes de scripts grandes e monolíticos, você está em desvantagem quando se trata de armazenamento em cache do navegador. Isso ocorre porque, quando você atualiza seu código próprio, seja atualizando pacotes ou enviando correções de bugs, todo o pacote se torna invalidado e precisa ser baixado novamente.

Ao dividir seus scripts, você não está apenas dividindo o trabalho de avaliação de script em tarefas menores, mas também está aumentando a probabilidade de que visitantes recorrentes obtenham mais scripts do cache do navegador, e não da 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 na produção e carregando-os com o atributo type=module, é necessário 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, o que envolve outra solicitação para c.js. Uma maneira de evitar isso é usar um bundler, mas não se esqueça de configurar seu bundler para dividir os scripts e distribuir o trabalho de avaliação deles.

Se você não quiser usar um bundler, outra maneira de contornar chamadas de módulo aninhadas é usar a dica de recurso modulepreload, 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 um feito complicado. A abordagem depende dos requisitos e das restrições do seu site. No entanto, ao dividir os scripts, o trabalho de avaliação deles é distribuído em várias tarefas menores. Isso dá à linha de execução principal a capacidade de processar 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 consomem muitos recursos e bloqueiam a linha de execução principal. Distribua seus scripts por mais elementos <script> para dividir esse trabalho.
  • O uso do atributo type=module para carregar módulos ES de forma nativa no navegador inicia tarefas individuais para avaliação de cada script de módulo separado.
  • Reduza o tamanho dos pacotes iniciais usando chamadas import() dinâmicas. Isso também funciona em bundlers, já que os bundlers 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.
  • Avalie as vantagens e desvantagens como a eficiência da compactação e a invalidação de cache. Scripts maiores serão melhor comprimidos, mas é mais provável que envolvam trabalhos de avaliação de script mais caros em menos tarefas e resultem na invalidação do cache do navegador, levando a uma eficiência geral menor em cache.
  • Se você estiver usando módulos ES de forma nativa sem agrupar, 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 ato de equilíbrio, com certeza, mas ao dividir scripts e reduzir os payloads iniciais com o import() dinâmico, você pode 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 deve 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.