Ao carregar scripts, o navegador leva tempo para avaliá-los antes da execução, o que pode causar tarefas longas. Saiba como funciona a avaliação de scripts e o que você pode fazer para evitar que ela cause tarefas longas durante o carregamento da página.
Ao otimizar a Interaction to Next Paint (INP), a maioria das dicas que você vai encontrar é para otimizar as próprias interações. Por exemplo, no guia para otimizar tarefas longas, são discutidas técnicas como a geração com setTimeout e outras. Essas técnicas são benéficas, pois permitem que o fluxo principal tenha um pouco de espaço para respirar, evitando tarefas longas, o que pode proporcionar mais oportunidades para interações e outras atividades ocorrerem mais cedo, em vez de terem que esperar por uma única tarefa longa.
No entanto, e as tarefas longas que vêm do carregamento dos próprios scripts? Essas tarefas podem interferir nas interações do usuário e afetar o INP de uma página durante o carregamento. Este guia vai explicar como os navegadores processam tarefas iniciadas pela avaliação de script e o que você pode fazer para dividir o trabalho de avaliação de script para que sua linha de execução principal possa responder melhor à entrada do usuário enquanto a página está carregando.
O que é a avaliação de scripts?
Se você criou o perfil de um aplicativo que envia muito JavaScript, talvez tenha visto tarefas longas em que o culpado é rotulado como Avaliar script.
A avaliação de script é uma parte necessária da execução de JavaScript no navegador, já que o JavaScript é compilado just-in-time antes da execução. Quando um script é avaliado, ele é analisado primeiro para erros. Se o analisador não encontrar erros, o script será compilado em bytecode e poderá continuar para a execução.
Embora seja 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 carregar. As interações que ocorrem durante o carregamento podem ser atrasadas porque a página está ocupada avaliando scripts. Embora não haja garantia de que uma interação possa ocorrer neste momento, já que um script responsável por ela ainda não pode ter sido carregado, pode haver interações dependentes de JavaScript que estão prontas ou a interatividade não depende de JavaScript.
A relação entre scripts e as tarefas que os avaliam
A forma como as tarefas responsáveis pela avaliação de script são iniciadas depende se o script que você está carregando é carregado com um elemento <script> típico ou se é um módulo carregado com o type=module. Como os navegadores tendem a lidar com as coisas de maneira diferente, vamos abordar como os principais mecanismos de navegador lidam com a avaliação de scripts quando os comportamentos de avaliação de scripts 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, de modo que ele possa ser analisado, compilado e executado. É o caso dos navegadores baseados no Chromium, Safari, e Firefox.
Por que isso é importante? Digamos que você esteja usando um bundler para gerenciar seus scripts de produção e o tenha configurado para agrupar tudo o que sua página precisa para ser executada em um único script. Se for esse o caso do seu site, espere que uma única tarefa seja enviada para avaliar o script. Isso é algo ruim? Não necessariamente, a menos que o script seja enorme.
É possível dividir o trabalho de avaliação de script evitando o carregamento de grandes partes de JavaScript e carregando mais scripts individuais menores usando elementos <script> adicionais.
Embora você sempre deva tentar carregar o mínimo possível de JavaScript durante o carregamento de página, dividir os scripts garante que, em vez de uma tarefa grande que pode bloquear a linha de execução principal, você tenha um número maior de tarefas menores que não vão bloquear a linha de execução principal ou pelo menos menos do que você começou.
<script> presentes no HTML da página. Isso é preferível a enviar um pacote de script grande aos usuários, o que tem mais chances de bloquear a linha de execução principal.
Dividir as tarefas para avaliação de script é semelhante a gerar durante callbacks de eventos que são executados durante uma interação. No entanto, com a avaliação de script, o mecanismo de geração divide o JavaScript carregado em vários scripts menores, em vez de um número menor de scripts maiores 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 nativamente no navegador com o type=module attribute no elemento <script>. Essa abordagem para carregamento de script traz alguns benefícios para a experiência do desenvolvedor, como não precisar transformar o código para uso em produção, principalmente quando usada em combinação com mapas de importação. No entanto, carregar scripts dessa forma agenda tarefas que variam de navegador para navegador.
Navegadores baseados no Chromium
Em navegadores como o Chrome ou derivados dele, o carregamento de módulos ES usando o atributo type=module produz diferentes tipos de tarefas do que você normalmente veria ao não usar type=module. Por exemplo, uma tarefa para cada script de módulo será executada, envolvendo uma atividade rotulada como Compilar módulo.
Depois que os módulos forem compilados, qualquer código executado neles vai iniciar uma atividade rotulada como Avaliar módulo.
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á incorrendo em algum custo inevitável. Embora você deva tentar enviar o mínimo possível de JavaScript, usar módulos ES, seja qual for o 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 estrito.
- Os scripts carregados usando
type=modulesão tratados como se fossem adiados por padrão. É possível usar o atributoasyncem scripts carregados comtype=modulepara 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, você pode 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 avaliação.
Scripts carregados com import() dinâmico
Dynamic import() é outro método para carregar 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 de um script para carregar um trecho de JavaScript sob demanda. Essa técnica é chamada de divisão de código.
O import() dinâmico tem duas vantagens quando se trata de melhorar o INP:
- Os módulos que são adiados para serem carregados mais tarde reduzem a disputa da linha de execução principal durante a inicialização, diminuindo a quantidade de JavaScript carregado nesse momento. Isso libera a linha de execução principal para que ela possa responder melhor às interações do usuário.
- Quando as chamadas dinâmicas de
import()são feitas, cada chamada separa a compilação e a avaliação de cada módulo para a própria tarefa. É claro que umimport()dinâmico que carrega um módulo muito grande vai iniciar uma tarefa de avaliação de script bastante grande, o que 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 deimport()dinâmica. Portanto, ainda é muito importante carregar o mínimo possível de JavaScript.
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.
Scripts carregados em um service worker
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 do worker é executado na própria linha de execução. Isso é muito benéfico porque, embora o código que registra o service worker da Web 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 ajuda a mantê-la mais responsiva às interações do usuário.
Além de reduzir o trabalho da linha de execução principal, os web workers podem carregar scripts externos para serem usados no contexto do worker, seja por importScripts ou instruções import estáticas em navegadores que oferecem suporte a workers de módulo. O resultado é que qualquer script solicitado por um service worker é avaliado fora da linha de execução principal.
Vantagens e desvantagens e considerações
Embora dividir os scripts em arquivos separados e menores ajude a limitar tarefas longas em vez de carregar menos arquivos, mas muito maiores, é importante levar algumas coisas em consideração ao decidir como dividir os scripts.
Eficiência de compactação
A compactação é um fator importante na divisão de scripts. Quando os scripts são menores, a compactação se torna um pouco menos eficiente. Scripts maiores se beneficiam muito mais da compactação. Embora o aumento da eficiência da compactação ajude a manter os tempos de carregamento dos scripts o mais baixo possível, é preciso equilibrar 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 caso do webpack, o plug-in
SplitChunksPluginpode ajudar. Consulte a documentação doSplitChunksPluginpara ver as opções que podem ser definidas e ajudar a gerenciar os tamanhos dos recursos. - Para outros agrupadores, como Rollup e esbuild, é possível gerenciar os tamanhos dos arquivos de script usando chamadas dinâmicas de
import()no código. Esses agrupadores, assim como o webpack, vão separar automaticamente o recurso importado dinamicamente em um arquivo próprio, evitando tamanhos maiores de pacote inicial.
Invalidação de cache
A invalidação do 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ê fica em desvantagem quando se trata do processo de cache do navegador. Isso acontece porque, quando você atualiza seu código próprio (atualizando pacotes ou enviando correções de bugs), todo o pacote é invalidado e precisa ser baixado novamente.
Ao dividir os scripts, você não está apenas dividindo o trabalho de avaliação em tarefas menores, mas também aumentando a probabilidade de que os visitantes recorrentes peguem mais scripts do cache do navegador em vez da rede. Isso se traduz em um carregamento de página mais rápido no geral.
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, precisa saber como o aninhamento de módulos pode afetar o tempo de inicialização. O aninhamento de módulos ocorre 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 é solicitado de um elemento <script>, outra solicitação de rede é enviada para b.js, que envolve outra solicitação para c.js. Uma maneira de evitar isso é usar um bundler. No entanto, configure-o para dividir os scripts e distribuir o trabalho de avaliação.
Se você não quiser usar um bundler, outra maneira de contornar as 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, uma tarefa difícil. A abordagem depende dos requisitos e restrições do seu site. No entanto, ao dividir os scripts, você distribui o trabalho de avaliação de script em várias tarefas menores e, portanto, dá à linha de execução principal a capacidade de lidar com as interações do usuário de maneira mais eficiente, em vez de bloqueá-la.
Para recapitular, confira algumas coisas que você pode fazer para dividir tarefas grandes de avaliação de script:
- Ao carregar scripts usando o elemento
<script>sem o atributotype=module, evite carregar scripts muito grandes, já que 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 em mais elementos<script>para dividir esse trabalho. - Usar o atributo
type=modulepara carregar módulos ES de forma nativa no navegador vai iniciar tarefas individuais de avaliação para cada script de módulo separado. - Reduza o tamanho dos pacotes iniciais usando chamadas dinâmicas de
import(). Isso também funciona em bundlers, já que 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. - Considere compensações, como eficiência de compactação e invalidação de cache. Scripts maiores são compactados melhor, mas têm mais chances de envolver um trabalho de avaliação de script mais caro em menos tarefas e resultar na invalidação do cache do navegador, o que leva a uma eficiência geral menor do cache.
- Se você estiver usando módulos ES de forma nativa sem agrupamento, use a dica de recurso
modulepreloadpara otimizar o carregamento deles durante a inicialização. - Como sempre, envie o mínimo de JavaScript possível.
É um ato de equilíbrio, com certeza, mas, ao dividir scripts e reduzir os payloads iniciais com 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. Isso vai ajudar você a ter uma pontuação melhor na métrica INP e, assim, oferecer uma experiência melhor ao usuário.