
⚡ 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
asyncedefer: 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:
1 function 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 |
|---|---|---|---|
| Paralelo com a análise | Imediatamente após o carregamento | Não garantida |
| Paralelo com a análise | Após a análise completa do HTML | Garantida (como no documento) |
Regra prática:
asyncpara scripts independentes: analytics, anúncios, contadores. Não precisam do DOM, não se importam com a ordem.deferpara 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 2 button.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 |
|---|---|---|---|---|
| Sim | Não | Garantida | Não usar a menos que necessário |
| Não | Sim | Via cadeia | Carregamento condicional, dependências pesadas |
| Não | Não | Não garantida | Analytics, anúncios, contadores |
| Não | Não | Garantida | Aplicação principal, manipulação do DOM |
| Não | Sim (módulo ES) | Via | 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
asyncexecuta o script imediatamente após o ficheiro carregar, mesmo que o HTML ainda não esteja totalmente analisado, e sem garantia de ordem. Odeferespera 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, usedefer; para contadores isolados, useasync.
Pode combinar carregamento dinâmico com defer?
Sim, é um cenário comum. O núcleo da aplicação carrega com
deferno<head>e inicializa a interface. Módulos pesados ou raramente usados são carregados vialoadScript()dinâmico ouimport()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
deferouasyncviawp_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 atributoasync. Blocos interativos complexos (calculadoras, filtros) devem ser movidos paraimport()dinâmico dentro de um módulo personalizado.
Isto funciona com scripts de terceiros como o Google Analytics?
Sim. A etiqueta GA4
gtag.jscarrega comasyncpor 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, useasyncà vontade. Se precisar do DOM, usedeferou 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
deferouasync, 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.



