Introduzione
In questo articolo ti insegnerò come caricare ed eseguire codice JavaScript nel browser.
No, aspetta, torna indietro. So che sembra banale e semplice, ma ricorda che questo accade nel browser, dove teoricamente semplice diventa una stranezza legata alla tradizione. Conoscere queste peculiarità ti consente di scegliere il modo più rapido e meno invasivo per caricare gli script. Se hai poco tempo, vai alla sezione di riferimento rapido.
Per iniziare, ecco come la specifica definisce i vari modi in cui uno script può essere scaricato ed eseguito:
Come tutte le specifiche WHATWG, inizialmente sembra il risultato di una bomba a grappolo in una fabbrica di Scrabble, ma dopo averla letta per la quinta volta e aver asciugato il sangue dagli occhi, è in realtà piuttosto interessante:
Il mio primo script include
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Che dolce semplicità. In questo caso, il browser scaricherà entrambi gli script in parallelo ed eseguirli il prima possibile, mantenendone l'ordine. "2.js" non verrà eseguito finché non sarà stato eseguito (o non avrà avuto esito negativo) "1.js", "1.js" non verrà eseguito finché non sarà stato eseguito lo script o lo stile precedente e così via.
Purtroppo, durante questo processo il browser blocca l'ulteriore rendering della pagina. Ciò è dovuto alle API DOM della "prima era del web", che consentono l'aggiunta di stringhe ai contenuti che l'analizzatore sintattico sta masticando, come document.write
. I browser più recenti continueranno ad analizzare o analizzare il documento in background e ad attivare i download per i contenuti esterni eventualmente necessari (js, immagini, CSS e così via), ma il rendering rimane bloccato.
Per questo motivo, i professionisti del rendimento consigliano di inserire gli elementi di script alla fine del documento, in modo da bloccare il minor numero possibile di contenuti. Purtroppo significa che il tuo script non viene visualizzato dal browser finché non scarica tutto il codice HTML e a quel punto inizia a scaricare altri contenuti, come CSS, immagini e iframe. I browser moderni sono abbastanza intelligenti da dare la priorità a JavaScript alle immagini, ma noi possiamo fare di meglio.
Grazie IE! (no, non è sarcasmo)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Microsoft ha riconosciuto questi problemi di prestazioni e ha introdotto l'attributo "defer" in Internet Explorer 4. In pratica, significa "Prometto di non iniettare elementi nel parser utilizzando elementi come document.write
. Se infrango questa promessa, sei libero di punirmi in qualsiasi modo tu ritenga opportuno". Questo attributo è stato convertito in HTML4 ed è apparso in altri browser.
Nell'esempio precedente, il browser scaricherà entrambi gli script in parallelo e li eseguirà poco prima dell'attivazione di DOMContentLoaded
, mantenendo l'ordine.
Come una bomba a grappolo in una fabbrica di pecore, "rimandare" è diventato un pasticcio lanoso. Tra gli attributi "src" e "defer" e i tag script rispetto agli script aggiunti dinamicamente, abbiamo 6 pattern di aggiunta di uno script. Ovviamente, i browser non erano d'accordo sull'ordine di esecuzione. Mozilla ha scritto un ottimo articolo sul problema, come già nel 2009.
WHATWG ha reso esplicito il comportamento, dichiarando che "defer" non ha alcun effetto sugli script aggiunti dinamicamente o privi di "src". In caso contrario, gli script differiti devono essere eseguiti dopo l'analisi del documento, nell'ordine in cui sono stati aggiunti.
Grazie IE! (ok, ora sto scherzando)
Regala, toglie. Purtroppo, in IE4-9 esiste un brutto bug che può causare l'esecuzione degli script in un ordine imprevisto. Ecco cosa succede:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
Supponendo che ci sia un paragrafo nella pagina, l'ordine previsto dei log è [1, 2, 3], anche se in IE9 e sotto si ottiene [1, 3, 2]. Operazioni DOM specifiche causano la messa in pausa dell'esecuzione dello script corrente e l'esecuzione di altri script in attesa prima di continuare.
Tuttavia, anche in implementazioni senza bug, come IE10 e altri browser, l'esecuzione dello script viene ritardata fino al download e all'analisi dell'intero documento. Questa opzione può essere comoda se comunque intendi attendere DOMContentLoaded
, ma se vuoi ottenere un rendimento davvero elevato, puoi iniziare ad aggiungere ascoltatori e avviare l'avvio iniziale prima…
HTML5 viene in soccorso
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
HTML5 ci ha fornito un nuovo attributo, "asinc", che presuppone che non utilizzerai document.write
, ma non attende che il documento venga analizzato prima di essere eseguito. Il browser scaricherà entrambi gli script in parallelo ed eseguirli il prima possibile.
Purtroppo, poiché verranno eseguiti il prima possibile, "2.js" potrebbe essere eseguito prima di "1.js". Non c'è problema se sono indipendenti, ad esempio se "1.js" è uno script di monitoraggio che non ha nulla a che fare con "2.js". Ma se "1.js" è una copia CDN di jQuery da cui dipende "2.js", la tua pagina sarà ricoperta di errori, come una bomba a grappolo in un… non so… non ho niente per questo.
So cosa ci serve, una libreria JavaScript.
Fare in modo che una serie di script venga scaricata immediatamente senza bloccare il rendering e che vengano eseguiti il prima possibile nell'ordine in cui sono stati aggiunti. Purtroppo HTML ti odia e non ti permette di farlo.
Il problema è stato affrontato da JavaScript in alcune versioni. Alcune richiedevano di apportare modifiche al codice JavaScript, inserendo un callback chiamato dalla libreria nell'ordine corretto (ad es. RequireJS). Altri utilizzavano XHR per il download in parallelo e poi eval()
nell'ordine corretto, il che non funzionava per gli script su un altro dominio, a meno che non avessero un'intestazione CORS e il browser la supportasse. Alcuni hanno persino utilizzato hack super magici, come LabJS.
Gli hack prevedevano di indurre il browser a scaricare la risorsa in modo da attivare un evento al termine, ma evitando di eseguirlo. In LabJS, lo script verrebbe aggiunto con un tipo MIME errato, ad esempio <script type="script/cache" src="...">
. Una volta scaricati tutti gli script, questi venivano aggiunti di nuovo con un tipo corretto, sperando che il browser li recuperasse direttamente dalla cache ed eseguisse immediatamente, in ordine. Questo dipendeva da un comportamento pratico, ma non specificato, e si è interrotto quando HTML5 ha dichiarato che i browser non devono scaricare script con un tipo non riconosciuto. Vale la pena notare che LabJS si è adattato a questi cambiamenti e ora utilizza una combinazione dei metodi in questo articolo.
Tuttavia, i caricatori di script hanno un problema di prestazioni proprio: devi attendere che il codice JavaScript della libreria venga scaricato e analizzato prima che qualsiasi script gestito possa iniziare a essere scaricato. Inoltre, come faremo a caricare il caricatore di script? Come faremo a caricare lo script che dice al caricatore di script cosa caricare? Chi guarda Watchmen? Perché sono nuda? Sono tutte domande difficili.
In sostanza, se devi scaricare un file di script aggiuntivo prima ancora di pensare di scaricare altri script, hai già perso la battaglia per le prestazioni.
Il DOM in soccorso
La risposta è in realtà nella specifica HTML5, anche se è nascosta nella parte inferiore della sezione di caricamento dello script.
Traduzione in "Terreno":
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
Gli script creati e aggiunti in modo dinamico al documento sono asincroni per impostazione predefinita, non bloccano il rendering e non vengono eseguiti non appena vengono scaricati, il che significa che potrebbero uscire nell'ordine sbagliato. Tuttavia, possiamo contrassegnarli esplicitamente come non asincroni:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
In questo modo, i nostri script hanno un comportamento misto che non può essere ottenuto con il codice HTML. Poiché sono esplicitamente non asincroni, gli script vengono aggiunti a una coda di esecuzione, ovvero alla stessa coda a cui sono stati aggiunti nel nostro primo esempio in formato HTML normale. Tuttavia, poiché vengono creati dinamicamente, vengono eseguiti al di fuori dell'analisi del documento, quindi il rendering non viene bloccato durante il download (non confondere il caricamento di script non asincroni con XHR sincrono, che non è mai una buona idea).
Lo script riportato sopra deve essere incluso in linea nell'intestazione delle pagine, mettere in coda i download degli script il prima possibile senza interrompere il rendering progressivo ed essere eseguito il prima possibile nell'ordine specificato. Il download di “2.js” è senza costi prima di “1.js”, ma non verrà eseguito fino a quando “1.js” non è stato scaricato ed eseguito correttamente, oppure non riesce a farlo. Evviva! async-download, ma con esecuzione ordinata.
Il caricamento degli script in questo modo è supportato da tutto ciò che supporta l'attributo async, ad eccezione di Safari 5.0 (5.1 è OK). Inoltre, sono supportate tutte le versioni di Firefox e Opera che non supportano l'attributo async, in quanto eseguono comunque gli script aggiunti dinamicamente nell'ordine in cui vengono aggiunti al documento.
È il modo più veloce per caricare gli script, giusto? molto utile.
Beh, se decidi in modo dinamico quali script caricare, sì, altrimenti forse no. Nell'esempio riportato sopra, il browser deve analizzare ed eseguire lo script per scoprire quali script scaricare. In questo modo i tuoi script vengono nascosti agli scanner di precaricamento. I browser utilizzano questi scanner per trovare le risorse nelle pagine che probabilmente visiterai successivamente o per trovare le risorse della pagina mentre il parser è bloccato da un'altra risorsa.
Possiamo aggiungere nuovamente la rilevabilità inserendo questo codice nell'intestazione del documento:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Questo indica al browser che la pagina ha bisogno di 1.js e 2.js. link[rel=subresource]
è simile a link[rel=prefetch]
, ma con semantica diversa. Purtroppo al momento è supportato solo in Chrome e devi dichiarare quali script caricare due volte, una volta tramite elementi link e di nuovo nello script.
Correzione: inizialmente ho affermato che questi elementi venivano rilevati dallo scanner di precarica, ma non è così, vengono rilevati dal parser normale. Tuttavia, lo scanner di precaricamento potrebbe rilevarli, ma non lo fa ancora, mentre gli script inclusi dal codice eseguibile non possono mai essere precaricati. Grazie a Yoav Weiss che mi ha corretto nei commenti.
Questo articolo mi deprime
La situazione è deprimente e dovresti sentirti depresso. Non esiste un modo non ripetitivo, ma dichiarativo, per scaricare gli script in modo rapido e asincrono, controllando al contempo l'ordine di esecuzione. Con HTTP2/SPDY puoi ridurre l'overhead delle richieste al punto che l'invio di script in più piccoli file singolarmente memorizzabili nella cache può essere il modo più veloce. Ecco un possibile scenario:
<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>
Ogni script di miglioramento gestisce un determinato componente della pagina, ma richiede funzioni di utilità in dependencies.js. Idealmente, vogliamo scaricare tutti gli script in modo asincrono, quindi eseguire gli script di miglioramento il prima possibile, in qualsiasi ordine, ma dopo dipendenze.js. È un potenziamento progressivo progressivo. Purtroppo non esiste un modo dichiarativo per farlo, a meno che gli script stessi non vengano modificati per monitorare lo stato di caricamento di dependencies.js. Anche async=false non risolve il problema, poiché l'esecuzione di enhancement-10.js si bloccherà su 1-9. In effetti, esiste un solo browser che lo rende possibile senza trucchi…
IE ha un'idea.
IE carica gli script in modo diverso rispetto agli altri browser.
var script = document.createElement('script');
script.src = 'whatever.js';
IE inizia a scaricare "whatever.js" ora, mentre gli altri browser non avviano il download finché lo script non è stato aggiunto al documento. IE ha anche un evento, "readystatechange", e una proprietà, "readystate", che ci indicano l'avanzamento del caricamento. Questo è molto utile, in quanto ci consente di controllare il caricamento e l'esecuzione degli script in modo indipendente.
var script = document.createElement('script');
script.onreadystatechange = function() {
if (script.readyState == 'loaded') {
// Our script has download, but hasn't executed.
// It won't execute until we do:
document.body.appendChild(script);
}
};
script.src = 'whatever.js';
Possiamo creare modelli di dipendenza complessi scegliendo quando aggiungere script al documento. IE supporta questo modello dalla versione 6. Molto interessante, ma presenta ancora lo stesso problema di rilevabilità del preloader di async=false
.
Basta! Come devo caricare gli script?
Ok ok. Se vuoi caricare gli script in modo che non blocchino il rendering, non richiedano ripetizioni e abbiano un'eccellente compatibilità con i browser, ecco cosa ti propongo:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Come sembra. Alla fine dell'elemento body. Sì, essere uno sviluppatore web è un po' come essere il re Sisifo (boom! 100 punti hipster per riferimento alla mitologia greca!). Le limitazioni di HTML e dei browser ci impediscono di fare molto di più.
Spero che i moduli JavaScript ci salvino fornendo un modo dichiarativo e non bloccante per caricare gli script e dare il controllo sull'ordine di esecuzione, anche se questo richiede che gli script vengano scritti come moduli.
Eww, ci deve essere qualcosa di meglio che possiamo usare ora?
Giusto, per ottenere punti bonus, se vuoi ottenere un rendimento davvero elevato e non ti dispiace un po' di complessità e ripetizione, puoi combinare alcuni dei trucchi sopra indicati.
Innanzitutto, aggiungiamo la dichiarazione della risorsa secondaria per i preloader:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Poi, in linea nell'elemento head del documento, carichiamo i nostri script con JavaScript, utilizzando async=false
, passando al caricamento dello script basato su readystate di IE, passando a defer.
var scripts = [
'1.js',
'2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we'll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
Con alcuni suggerimenti e minimizzazioni più avanti, ci sono 362 byte + gli URL dello script:
!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
"//other-domain.com/1.js",
"2.js"
])
Vale la pena utilizzare i byte aggiuntivi rispetto a una semplice inclusione di script? Se stai già utilizzando JavaScript per caricare gli script in modo condizionale, come fa la BBC, potresti trarre vantaggio dall'attivazione di questi download in precedenza. In caso contrario, forse è meglio utilizzare il semplice metodo di fine del corpo.
Fiuuu, ora so perché la sezione di caricamento dello script WHATWG è così vasta. Ho bisogno di un drink.
Riferimento rapido
Elementi di script semplici
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Le specifiche indicano: scarica insieme, esegui in ordine dopo eventuali CSS in attesa, blocca il rendering fino al completamento. I browser dicono: Sì, signore.
Rimanda
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
La specifica dice: scarica insieme, esegui in ordine appena prima di DOMContentLoaded. Ignora "defer" negli script senza "src". IE < 10 dice: potrei eseguire 2.js a metà dell'esecuzione di 1.js. Non è divertente? I browser in rosso dicono: non ho idea di cosa sia questa cosa "posticipa", caricherò gli script come se non ci fossero. Altri browser dicono: Ok, ma potrei non ignorare l'opzione "Differisci" per gli script senza "src".
Asinc
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
Le specifiche dicono: Scarica i dati insieme, eseguili nell'ordine in cui vengono scaricati. I browser in rosso dicono: che cos'è "async"? Caricherò gli script come se non fossero presenti. Gli altri browser dicono: Sì, ok.
Asinc (falso)
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
La specifica dice: scarica insieme, esegui in ordine non appena tutto è stato scaricato. Firefox < 3.6, Opera dice: Non ho idea di cosa sia questa cosa "asincrona", ma eseguio solo degli script aggiunti tramite JS nell'ordine in cui sono aggiunti. Safari 5.0 dice: capisco "async", ma non capisco come impostarlo su "false" con JS. Eseguirò i tuoi script non appena vengono pubblicati, in qualsiasi ordine. IE < 10 dice: Non ho idea di cosa sia "async", ma esiste una soluzione alternativa che utilizza "onreadystatechange". Altri browser in rosso dicono: Non capisco questa cosa di "async", eseguirò i tuoi script non appena vengono caricati, in qualsiasi ordine. Tutto il resto dice: sono tuo amico, lo faremo nel rispetto delle regole.