Skip to content

Tudo para WordPress, desenvolvimento web — e não só

⚡ Como carregar JavaScript externo sem bloquear a página

⚡ Como carregar JavaScript externo sem bloquear a página

Quando o browser encontra uma etiqueta <script> sem atributos, larga tudo. A renderização da página pára completamente até que o script seja carregado e executado. Numa ligação 4G lenta, isso significa 2 a 3 segundos de ecrã em branco.

Nessa altura, o utilizador já foi para um concorrente. O Core Web Vitals regista um LCP falhado, o Google empurra a página para baixo nos resultados de pesquisa e você perde tráfego e conversões. Entretanto, o problema resolve-se com três linhas, se souber onde procurar.

Abaixo encontra um método funcional para carregar JavaScript externo sem bloqueio. Desde a abordagem clássica de dois ficheiros até ao moderno async/defer e import() dinâmico. Com código comprovado que pode copiar e colar.

💡 Resumo rápido:

  • Compreender o problema: como um <script> normal bloqueia a análise de HTML e mata a velocidade de carregamento
  • Dominar a abordagem clássica: um pequeno carregador (≤300 bytes) que carrega dinamicamente o ficheiro JS principal
  • Aprender os atributos nativos async e defer: quando e qual usar
  • Explorar o import() dinâmico para carregar módulos a pedido
  • Escolher uma estratégia para o seu projeto com uma tabela comparativa

Porque é que o JavaScript bloqueia a renderização

Quando o analisador de HTML chega a <script src="app.js">, faz exatamente três coisas: para de analisar o documento, transfere o ficheiro, executa-o. Só depois continua a construir o DOM.

A razão é arquitetural. Um script pode conter document.write(), que altera o HTML em tempo real. O browser não sabe antecipadamente se existe essa chamada, por isso joga pelo seguro e espera pelo carregamento e execução completos. O resultado: até um script leve de 5 KB adiciona centenas de milissegundos ao First Contentful Paint só por causa de um único ciclo de rede.

O problema não é novo. Em 2009, Nicholas Zakas descreveu uma técnica para carregamento dinâmico de JavaScript sem bloqueio, e ainda funciona hoje, embora com ajustes para APIs modernas. Com a chegada de async, defer e módulos ES, os programadores têm agora um conjunto completo de ferramentas. Vamos examinar cada uma.

Abordagem clássica: dois ficheiros e carregamento dinâmico

A ideia é simples. Em vez de colocar todo o JS num ficheiro e anexá-lo à página via <script src="...">, divide o código em duas partes:

  • Um pequeno carregador (200-300 bytes após compressão)
  • O ficheiro principal com a lógica da aplicação

O carregador é inserido inline no final da página, mesmo antes de </body>. Cria um <script> programaticamente e adiciona-o ao DOM; essa etiqueta já não bloqueia a análise porque aparece fora do fluxo principal do documento. Assim que o ficheiro principal carrega, a inicialização é executada.

Versão moderna da função em JS puro, sem retrocompatibilidade com IE:

1function loadScript(url) {
2 return new Promise((resolve, reject) => {
3 const script = document.createElement('script');
4 script.src = url;
5 script.onload = resolve;
6 script.onerror = reject;
7 document.head.appendChild(script);
8 });
9}

Nove linhas. Sem verificações de readyState, sem ramificações para IE antigo, sem pirâmide de callbacks. Apenas uma função que devolve uma Promise, conveniente para combinar com async/await.

A utilização na página tem este aspeto (código no final, antes do </body> de fecho):

1<script>
2 function loadScript(url) {
3 return new Promise((resolve, reject) => {
4 const script = document.createElement('script');
5 script.src = url;
6 script.onload = resolve;
7 script.onerror = reject;
8 document.head.appendChild(script);
9 });
10 }
11
12 loadScript('/js/app.js').then(() => {
13 // Initialize after main file loads
14 App.init();
15 });
16</script>

O primeiro script (inline) é o carregador. É analisado e executado instantaneamente porque tem menos de 300 bytes. O segundo script (app.js) carrega de forma assíncrona e não interfere com a renderização.

E se tiver mais de dois ficheiros? Combine-os durante o build. Empacotadores modernos como Vite e Webpack fazem isto automaticamente: tree-shaking, divisão de código, minificação numa só passagem. Gerir manualmente a ordem de carregamento de uma dúzia de ficheiros é um caminho para condições de corrida e erros.

Async e defer: desbloqueio nativo

O HTML5 deu-nos dois atributos que resolvem o problema sem uma única linha de JavaScript:

1<script async src="analytics.js"></script>
2<script defer src="app.js"></script>

Ambos carregam o ficheiro em paralelo com a análise do HTML. A diferença está no momento da execução:

Atributo

Carregamento

Execução

Ordem

async

Paralelo com a análise

Imediatamente após o carregamento

Não garantida

defer

Paralelo com a análise

Após a análise completa do HTML

Garantida (como no documento)

Regra prática:

  • async para scripts independentes: analytics, anúncios, contadores. Não precisam do DOM, não se importam com a ordem.
  • defer para a aplicação principal: manipulação do DOM, inicialização da interface. O script espera pela prontidão da página e executa na sequência correta.

Na prática, a combinação é simples: coloque defer em todos os scripts no <head>, e eles comportam-se como se estivessem no final da página, mas carregam mais cedo. Não há magia, apenas o agendador do browser.

E sim, pode combinar com carregamento dinâmico. Por exemplo, carregue o núcleo da aplicação via <script defer>, e anexe widgets pesados dinamicamente através de loadScript() apenas quando forem realmente necessários.

Import() dinâmico: módulos a pedido

O ES2020 trouxe o import() dinâmico, uma forma nativa de carregar um módulo de forma assíncrona, sem empacotador e sem funções extra:

1// Loads only when user clicked
2button.addEventListener('click', async () => {
3 const { heavyChart } = await import('./chart-component.js');
4 heavyChart.render();
5});

A chamada import() devolve uma Promise. O módulo carrega em segundo plano, a análise não é bloqueada, a página mantém-se responsiva. O código dentro do módulo executa em modo estrito e no seu próprio escopo, eliminando conflitos de nomes.

Esta é a ferramenta ideal para divisão de código sem empacotador. Componentes pesados (gráficos, editores, mapas) são movidos para ficheiros separados e carregados na primeira interação. Um utilizador que nunca abre um gráfico não paga por ele com tráfego e tempo de carregamento.

Comparação de abordagens

Cada método tem o seu nicho. Para evitar adivinhações, compilámos as características numa tabela:

Abordagem

Bloqueia renderização

Requer JS

Ordem de execução

Para que scripts

<script src>

Sim

Não

Garantida

Não usar a menos que necessário

loadScript dinâmico

Não

Sim

Via cadeia .then()

Carregamento condicional, dependências pesadas

<script async>

Não

Não

Não garantida

Analytics, anúncios, contadores

<script defer>

Não

Não

Garantida

Aplicação principal, manipulação do DOM

import()

Não

Sim (módulo ES)

Via await

Divisão de código, componentes a pedido

Principal conclusão: não se fixe num único método. Uma configuração de produção típica usa dois ou três em simultâneo: defer para o núcleo, async para métricas, import() dinâmico para componentes pesados.

Vídeo de demonstração curto sobre o tema, análise de async e defer com visualização da linha temporal de carregamento:

⁉️🤔 Perguntas frequentes

Como é que o async difere do defer na prática?

Ambos não bloqueiam a análise durante o carregamento. Mas o async executa o script imediatamente após o ficheiro carregar, mesmo que o HTML ainda não esteja totalmente analisado, e sem garantia de ordem. O defer espera sempre pela prontidão total do DOM e preserva a sequência dos scripts como no HTML. Para o código principal da aplicação, use defer; para contadores isolados, use async.

Pode combinar carregamento dinâmico com defer?

Sim, é um cenário comum. O núcleo da aplicação carrega com defer no <head> e inicializa a interface. Módulos pesados ou raramente usados são carregados via loadScript() dinâmico ou import() na interação do utilizador. Assim, obtém um arranque rápido e o carregamento diferido de código secundário.

O que devo usar para um site WordPress?

O WordPress adiciona automaticamente defer ou async via wp_enqueue_script() se passar o argumento apropriado no quinto parâmetro: wp_enqueue_script('my-script', $url, [], null, ['strategy' => 'defer']). Para scripts de terceiros (Google Analytics, anúncios), a abordagem mais fácil é o atributo async. Blocos interativos complexos (calculadoras, filtros) devem ser movidos para import() dinâmico dentro de um módulo personalizado.

Isto funciona com scripts de terceiros como o Google Analytics?

Sim. A etiqueta GA4 gtag.js carrega com async por padrão, por isso não bloqueia a página. Para outros serviços de terceiros, verifique a documentação: se o script não precisar de um DOM pronto e não depender da ordem de carregamento, use async à vontade. Se precisar do DOM, use defer ou carregamento dinâmico com um callback.

Como verifico que um script realmente não bloqueia a página?

Abra Chrome DevTools → Performance → Record → recarregue a página. Na linha temporal, procure blocos amarelos "Scripting" antes do verde "First Contentful Paint". Se um script for carregado com defer ou async, a sua execução estará depois do FCP. O Lighthouse no modo "Performance" mostrará a recomendação "Remove render-blocking resources", e não deve haver scripts nessa lista.

Vale a pena mudar a sua abordagem de carregamento agora mesmo

Se os seus scripts ainda estão pendurados no <head> sem atributos, está a perder posições nos motores de busca e a irritar os utilizadores. Isto não é uma hipótese, o Lighthouse e o PageSpeed Insights mostram o problema a vermelho nas primeiras linhas do relatório.

Início rápido: percorra as etiquetas <script> no seu template, adicione defer para o código principal e async para as métricas. Demora cinco minutos, e o LCP pode melhorar 300 a 500 ms. A seguir, import() dinâmico para componentes pesados quando tiver oportunidade de refatorar.

Deixe um método de carregamento, <script defer> no <head>, e a página carregará sem atrasos visíveis. Teste-o no seu projeto hoje.