Skip to content

Tutto per WordPress, lo sviluppo web — e non solo

⚡ Come caricare JavaScript esterno senza bloccare la pagina

⚡ Come caricare JavaScript esterno senza bloccare la pagina

Quando il browser incontra un tag <script> senza attributi, molla tutto. Il rendering della pagina si blocca completamente finché lo script non viene caricato ed eseguito. Su una connessione 4G lenta, sono 2-3 secondi di schermo bianco.

A quel punto l'utente è già passato a un concorrente. I Core Web Vitals registrano un LCP fallito, Google abbassa la pagina nei risultati di ricerca e perdi traffico e conversioni. Nel frattempo, il problema si risolve con tre righe se sai dove guardare.

Ecco un metodo funzionante per caricare JavaScript esterno senza bloccare il rendering. Dall'approccio classico a due file ai moderni async/defer e all'import() dinamico. Con codice testato che puoi copiare e incollare.

💡 Panoramica rapida:

  • Capire il problema: come un <script> normale blocca il parsing HTML e uccide la velocità di caricamento
  • Padroneggiare l'approccio classico: un loader minuscolo (≤300 byte) carica dinamicamente il file JS principale
  • Imparare gli attributi nativi async e defer: quando e quale usare
  • Esplorare l'import() dinamico per caricare moduli su richiesta
  • Scegliere una strategia per il tuo progetto con una tabella comparativa

Perché JavaScript blocca il rendering

Quando il parser HTML arriva a <script src="app.js">, fa esattamente tre cose: interrompe il parsing del documento, scarica il file, lo esegue. Solo dopo riprende a costruire il DOM.

Il motivo è architetturale. Uno script può contenere document.write(), che modifica l'HTML al volo. Il browser non sa in anticipo se c'è una chiamata del genere, quindi va sul sicuro e aspetta il caricamento e l'esecuzione completi. Risultato: anche uno script leggero da 5 KB aggiunge centinaia di millisecondi al First Contentful Paint solo per il round-trip di rete.

Il problema non è nuovo. Già nel 2009 Nicholas Zakas descrisse una tecnica per il caricamento dinamico non bloccante di JavaScript, e funziona ancora oggi, con gli adattamenti per le API moderne. Con l'arrivo di async, defer e dei moduli ES, gli sviluppatori hanno ora un toolkit completo. Esaminiamoli uno per uno.

Approccio classico: due file e caricamento dinamico

L'idea è semplice. Invece di mettere tutto il JS in un unico file e collegarlo alla pagina con <script src="...">, dividi il codice in due parti:

  • Un loader minuscolo (200-300 byte dopo compressione)
  • Il file principale con la logica applicativa

Il loader viene inserito inline in fondo alla pagina, subito prima di </body>. Crea un <script> in modo programmatico e lo aggiunge al DOM: un tag del genere non blocca più il parsing perché compare fuori dal flusso principale del documento. Appena il file principale si carica, l'inizializzazione viene eseguita.

Versione moderna della funzione in JS puro senza retrocompatibilità con 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 righe. Niente controlli readyState, niente rami per vecchie versioni di IE, niente callback a piramide. Solo una funzione che restituisce una Promise, comoda da combinare con async/await.

L'uso nella pagina si presenta così (codice in fondo, prima del </body> di chiusura):

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>

Il primo script (inline) è il loader. Viene parsato ed eseguito all'istante perché pesa meno di 300 byte. Il secondo script (app.js) si carica in modo asincrono e non interferisce con il rendering.

E se hai più di due file? Uniscili in fase di build. I bundler moderni come Vite e Webpack lo fanno in automatico: tree-shaking, code splitting, minificazione in un unico passaggio. Gestire a mano l'ordine di caricamento di una dozzina di file è la strada per race condition ed errori.

Async e defer: sblocco nativo

HTML5 ci ha dato due attributi che risolvono il problema senza una sola riga di JavaScript:

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

Entrambi caricano il file in parallelo al parsing HTML. La differenza sta nel momento dell'esecuzione:

Attributo

Caricamento

Esecuzione

Ordine

async

In parallelo al parsing

Subito dopo il caricamento

Non garantito

defer

In parallelo al parsing

Dopo il parsing HTML completo

Garantito (come nel documento)

Regola pratica:

  • async per script indipendenti: analytics, ads, contatori. Non hanno bisogno del DOM, non si preoccupano dell'ordine.
  • defer per l'applicazione principale: manipolazione del DOM, inizializzazione dell'interfaccia. Lo script aspetta che la pagina sia pronta e viene eseguito nella sequenza corretta.

In pratica, la combinazione è semplice: metti defer su tutti gli script nell'<head>, e si comportano come se fossero in fondo alla pagina ma si caricano prima. Nessuna magia, solo lo scheduler del browser.

E sì, puoi combinarlo con il caricamento dinamico. Per esempio, carica il core applicativo via <script defer>, e aggancia i widget pesanti dinamicamente con loadScript() solo quando servono davvero.

Import() dinamico: moduli su richiesta

ES2020 ha introdotto l'import() dinamico, un modo nativo per caricare un modulo in modo asincrono, senza bundler e senza funzioni extra:

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

La chiamata import() restituisce una Promise. Il modulo si carica in background, il parsing non viene bloccato, la pagina resta reattiva. Il codice dentro il modulo viene eseguito in strict mode e nel proprio scope, i conflitti di nomi sono eliminati.

Questo è lo strumento ideale per il code splitting senza bundler. I componenti pesanti (grafici, editor, mappe) vengono spostati in file separati e caricati alla prima interazione. Un utente che non apre mai un grafico non lo paga in termini di traffico e tempo di caricamento.

Confronto tra gli approcci

Ogni metodo ha la sua nicchia. Per evitare di tirare a indovinare, abbiamo raccolto le caratteristiche in una tabella:

Approccio

Blocca il rendering

Richiede JS

Ordine di esecuzione

Per quali script

<script src>

No

Garantito

Da non usare se non necessario

loadScript dinamico

No

Via catena .then()

Caricamento condizionale, dipendenze pesanti

<script async>

No

No

Non garantito

Analytics, ads, contatori

<script defer>

No

No

Garantito

Applicazione principale, manipolazione DOM

import()

No

Sì (modulo ES)

Via await

Code splitting, componenti su richiesta

Concetto chiave: non fissarti su un unico metodo. Un setup di produzione tipico ne usa due o tre contemporaneamente: defer per il core, async per la metrica, import() dinamico per i componenti pesanti.

Breve video dimostrativo sul tema, analisi di async e defer con visualizzazione della timeline di caricamento:

⁉️🤔 Domande frequenti

In pratica, che differenza c'è tra async e defer?

Entrambi non bloccano il parsing durante il caricamento. Ma async esegue lo script subito dopo il caricamento del file, anche se l'HTML non è ancora stato parsato del tutto, e senza garanzia di ordine. defer aspetta sempre il DOM completamente pronto e preserva la sequenza degli script come nell'HTML. Per il codice applicativo principale, usa defer, per contatori isolati, usa async.

Si può combinare il caricamento dinamico con defer?

Sì, è uno scenario comune. Il core applicativo si carica con defer nell'<head>, inizializza l'interfaccia. I moduli pesanti o usati di rado vengono caricati via loadScript() dinamico o import() all'interazione dell'utente. In questo modo ottieni sia un avvio veloce sia il caricamento differito del codice secondario.

Cosa uso per un sito WordPress?

WordPress aggiunge automaticamente defer o async tramite wp_enqueue_script() se passi l'argomento appropriato nel quinto parametro: wp_enqueue_script('my-script', $url, [], null, ['strategy' => 'defer']). Per script di terze parti (Google Analytics, ads), l'approccio più semplice è l'attributo async. I blocchi interattivi complessi (calcolatori, filtri) vanno spostati in import() dinamico dentro un modulo personalizzato.

Funziona con script di terze parti come Google Analytics?

Sì. Il tag GA4 gtag.js si carica con async di default, quindi non blocca la pagina. Per altri servizi di terze parti, controlla la documentazione: se lo script non richiede un DOM pronto e non dipende dall'ordine di caricamento, puoi usare tranquillamente async. Se ha bisogno del DOM, usa defer o il caricamento dinamico con una callback.

Come verifico che uno script non blocchi davvero la pagina?

Apri Chrome DevTools → Performance → Registra → ricarica la pagina. Sulla timeline, cerca i blocchi gialli "Scripting" prima del verde "First Contentful Paint". Se uno script è caricato con defer o async, la sua esecuzione sarà dopo l'FCP. Lighthouse in modalità "Performance" mostrerà la raccomandazione "Rimuovi le risorse che bloccano il rendering": in quella lista non devono comparire script.

Vale la pena cambiare approccio di caricamento adesso

Se i tuoi script restano ancora nell'<head> senza attributi, stai perdendo posizionamento sui motori di ricerca e infastidendo gli utenti. Non è un'ipotesi, Lighthouse e PageSpeed Insights mostrano il problema in rosso nelle prime righe del report.

Partenza rapida: passa in rassegna i tag <script> nel tuo template, aggiungi defer per il codice principale e async per la metrica. Ci vogliono cinque minuti, e l'LCP può migliorare di 300-500 ms. Poi, import() dinamico per i componenti pesanti quando avrai modo di fare refactoring.

Lascia un solo metodo di caricamento, <script defer> nell'<head>, e la pagina si caricherà senza ritardi visibili. Testalo sul tuo progetto oggi stesso.