Wprowadzenie
W tym artykule pokażę, jak wczytać i wykonywać kod JavaScript w przeglądarce.
Nie, wróć! Wiem, że brzmi to banalnie i prosto, ale pamiętaj, że to dzieje się w przeglądarce, gdzie teoretycznie proste staje się problemem z powodu starszych wersji. Dzięki znajomości tych specyfikacyjnych cech możesz wybrać najszybszy i najmniej uciążliwy sposób wczytywania skryptów. Jeśli masz napięty harmonogram, przejdź do sekcji Podręcznik.
Na początek zobacz, jak specyfikacja definiuje różne sposoby pobierania i wykonywania skryptu:

Podobnie jak wszystkie specyfikacje WHATWG, początkowo wygląda to jak efekt wybuchu bomby kasetowej w fabryce scrabble, ale gdy przeczytasz to po raz piąty i wytrzesz krew z oczu, okaże się, że jest to całkiem interesujące:
Mój pierwszy include skryptu
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Ach, błogość prostoty. W tym przypadku przeglądarka pobierze oba skrypty równolegle i wykona je tak szybko, jak to możliwe, zachowując ich kolejność. Plik „2.js” nie zostanie wykonany, dopóki nie zostanie wykonany plik „1.js” (lub nie uda się tego zrobić), a plik „1.js” nie zostanie wykonany, dopóki nie zostanie wykonany poprzedni skrypt lub sformatowana poprzednia szata graficzna itd.
Podczas tego procesu przeglądarka blokuje dalsze renderowanie strony. Wynika to z interfejsów DOM z „pierwszej ery internetu”, które umożliwiają dołączanie ciągów znaków do treści przetwarzanych przez parsownik, np. document.write
. Nowsze przeglądarki będą nadal skanować lub analizować dokument w tle i uruchamiać pobieranie treści zewnętrznych, których może potrzebować (js, obrazy, css itp.), ale renderowanie jest nadal zablokowane.
Dlatego eksperci od wydajności zalecają umieszczanie elementów skryptu na końcu dokumentu, ponieważ w ten sposób blokują oni jak najmniej treści. Oznacza to, że przeglądarka nie widzi skryptu, dopóki nie pobierze całego kodu HTML, a w tym czasie zaczyna pobierać inne treści, takie jak CSS, obrazy i ramki osadzone. Nowoczesne przeglądarki są na tyle inteligentne, że priorytetowo traktują JavaScript zamiast obrazów, ale możemy to jeszcze poprawić.
Dziękuję, IE. (nie żartuję)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Firma Microsoft zauważyła te problemy z wydajnością i wprowadziła funkcję „opóźnianie” w Internet Explorerze 4. Oznacza to w zasadzie „obiecuję, że nie wstrzyknę do parsowania żadnych danych za pomocą elementów takich jak document.write
. Jeśli złamię tę obietnicę, możesz mnie ukarać w dowolny sposób”. Ten atrybut został włączony do HTML4 i pojawił się w innych przeglądarkach.
W tym przykładzie przeglądarka pobierze oba skrypty równolegle i wykona je tuż przed wywołaniem funkcji DOMContentLoaded
, zachowując ich kolejność.
„Odłóż” stało się „odłóż się” – jak bomba kasetowa w fabryce owiec. W przypadku atrybutów „src” i „defer” oraz tagów skryptu i skryptów dodawanych dynamicznie mamy 6 wzorów dodawania skryptu. Oczywiście przeglądarki nie uzgodniły kolejności, w jakiej powinny wykonywać te instrukcje. Mozilla napisała świetny artykuł na temat tego problemu w 2009 r.
WHATWG określił to zachowanie w sposób jednoznaczny, oświadczając, że „opóźnienie” nie ma wpływu na skrypty, które zostały dodane dynamicznie lub nie mają atrybutu „src”. W przeciwnym razie opóźnione skrypty powinny być wykonywane po przeanalizowaniu dokumentu, w kolejności dodania.
Dziękuję, IE. (teraz żartuję)
Daje, ale też zabiera. W IE4-9 występuje niestety nieprzyjemny błąd, który może spowodować, że skrypty będą się wykonywać w nieoczekiwanej kolejności. Oto, co się dzieje:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
Zakładając, że na stronie jest akapit, spodziewany porządek logów to [1, 2, 3], ale w IE9 i starszych wersjach jest to [1, 3, 2]. Niektóre operacje DOM powodują, że Internet Explorer wstrzymuje bieżące wykonywanie skryptu i przed kontynuacją wykonuje inne oczekujące skrypty.
Jednak nawet w przypadku implementacji bez błędów, takich jak IE10 i inne przeglądarki, wykonanie skryptu jest opóźnione do momentu pobrania i przeanalizowania całego dokumentu. Może to być wygodne, jeśli i tak zamierzasz czekać na DOMContentLoaded
, ale jeśli zależy Ci na jak największej skuteczności, możesz zacząć dodawać słuchaczy i uruchamiać bootstrapping wcześniej.
HTML5 na ratunek
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
W HTML5 pojawił się nowy atrybut „async”, który zakłada, że nie będziesz używać funkcji document.write
, ale nie czeka na przeanalizowanie dokumentu. Przeglądarka pobierze oba skrypty równolegle i wykona je tak szybko, jak to możliwe.
Niestety, ponieważ mają być wykonywane tak szybko, jak to możliwe, plik „2.js” może zostać wykonany przed plikiem „1.js”. Nie ma to znaczenia, jeśli są one niezależne, a plik „1.js” to skrypt śledzenia, który nie ma nic wspólnego z plikiem „2.js”. Jeśli jednak plik „1.js” to kopia jQuery z CDN, na której polega plik „2.js”, Twoja strona zostanie pokryta błędami, jak ciastko w … nie wiem, co to za przykład.
Wiem, czego potrzebujemy: biblioteki JavaScript.
Święty Graal to natychmiastowe pobieranie zestawu skryptów bez blokowania renderowania i jak najszybsze ich wykonanie w kolejności dodania. Niestety HTML nie lubi Cię i nie pozwoli Ci tego zrobić.
Problem został rozwiązany za pomocą JavaScripta w kilku odmianach. Niektóre z nich wymagały wprowadzenia zmian w pliku JavaScript, np. umieszczenia go w funkcji zwracającej wywołanie zwrotne, którą biblioteka wywołuje w prawidłowej kolejności (np. RequireJS). Inni używali XHR do pobierania w paralelicznej kolejności, a potem eval()
w prawidłowej kolejności, co nie działało w przypadku skryptów w innej domenie, chyba że miały nagłówek CORS i przeglądarka je obsługiwała. Niektórzy używali nawet super-magicznych hacków, takich jak LabJS.
Hakerzy oszukali przeglądarkę, aby pobierała zasób w taki sposób, aby po jego zakończeniu wywołać zdarzenie, ale nie wykonywać go. W LabJS skrypt zostanie dodany z nieprawidłowym typem MIME, np. <script type="script/cache" src="...">
. Po pobraniu wszystkich skryptów zostaną one ponownie dodane z właściwym typem, mając nadzieję, że przeglądarka pobierze je bezpośrednio z pamięci podręcznej i natychmiast je wykona. Zależało to od wygodnego, ale nieokreślonego zachowania i nie działało, gdy przeglądarki z deklaracją HTML5 nie pobierały skryptów o nierozpoznanym typie. Warto zauważyć, że LabJS dostosował się do tych zmian i korzysta teraz z kombinacji metod opisanych w tym artykule.
Ładowarki skryptów mają jednak problemy z wydajnością. Zanim skrypty zaczną się pobierać, musisz poczekać, aż biblioteka JavaScript zostanie pobrana i przeanalizowana. Jak też załadujemy skrypt? Jak załadować skrypt, który mówi ładowarce skryptu, co ma wczytać? Kto obserwuje Strażników? Dlaczego jestem nagi? To trudne pytania.
Jeśli musisz pobrać dodatkowy plik skryptu, zanim zaczniesz myśleć o pobieraniu innych skryptów, już wtedy przegrywasz walkę o wydajność.
DOM na ratunek
Odpowiedź znajduje się w specyfikacji HTML5, ale jest ukryta na dole sekcji dotyczącej wczytywania skryptów.
Tłumaczenie na potrzeby „Ziemian”:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
Skrypty tworzone i dodawane dynamicznie do dokumentu są domyślnie asynchroniczne, co oznacza, że nie blokują renderowania i nie są wykonywane od razu po pobraniu, a więc mogą być wykonywane w niewłaściwej kolejności. Możemy jednak wyraźnie oznaczyć je jako nieasynchroniczne:
[
'//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);
});
Dzięki temu nasze skrypty mogą działać w sposób, którego nie da się osiągnąć za pomocą zwykłego kodu HTML. Ponieważ skrypty są wyraźnie nieasynchroniczne, są dodawane do kolejki wykonania, tej samej kolejki, do której zostały dodane w pierwszym przykładzie kodu HTML. Jednak ponieważ są tworzone dynamicznie, są wykonywane poza parsowaniem dokumentu, więc renderowanie nie jest blokowane podczas ich pobierania (nie myl ładowania skryptu asynchronicznego z synchronicznym żądaniem XHR, które nigdy nie jest dobre).
Powyższy skrypt powinien być umieszczony w główce strony, aby jak najszybciej umieścić w kole pobierania skrypty bez zakłócania płynnego renderowania i jak najszybciej go wykonać w określonej kolejności. Plik „2.js” można pobrać przed plikiem „1.js”, ale nie zostanie on wykonany, dopóki plik „1.js” nie zostanie pobrany i wykonany lub nie uda mu się tego dokonać. Hurrah! async-download but ordered-execution!
Ładowanie skryptów w ten sposób jest obsługiwane przez wszystkie przeglądarki, które obsługują atrybut async, z wyjątkiem Safari 5.0 (wersja 5.1 jest w porządku). Dodatkowo obsługiwane są wszystkie wersje Firefoxa i Opery, ponieważ wersje, które nie obsługują atrybutu async, wygodnie wykonują skrypty dodane dynamicznie w kolejności, w jakiej zostały dodane do dokumentu.
To najszybszy sposób wczytywania skryptów, prawda? Prawda?
Jeśli decydujesz dynamicznie, które skrypty mają się wczytać, to tak, ale w innych przypadkach może nie. W przypadku przykładu powyżej przeglądarka musi przeanalizować i wykonać skrypt, aby dowiedzieć się, które skrypty pobrać. Pozwala to ukryć skrypty przed skanerami wstępnego wczytywania. Przeglądarki używają tych skanerów do wykrywania zasobów na stronach, które prawdopodobnie odwiedzisz jako następne, lub do wykrywania zasobów strony, gdy parsowanie jest zablokowane przez inny zasób.
Możemy przywrócić wykrywalność, umieszczając w główce dokumentu ten fragment kodu:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
To mówi przeglądarce, że strona potrzebuje plików 1.js i 2.js. link[rel=subresource]
jest podobny do link[rel=prefetch]
, ale ma inne znaczenie. Obecnie jest ona obsługiwana tylko w Chrome. Musisz zadeklarować, które skrypty mają być wczytane dwukrotnie: raz za pomocą elementów linku, a potem ponownie w swoim skrypcie.
Poprawka: pierwotnie stwierdziliśmy, że te błędy były wykrywane przez skaner wstępnego ładowania, ale tak nie jest. Wykrywał je zwykły parsujący. Skaner wstępnego wczytywania może je jednak wykryć, ale jeszcze tego nie zrobił, podczas gdy skrypty zawarte w edytowalnym kodzie nie mogą być wstępnie wczytane. Dziękuję Yoav Weiss za sprostowanie w komentarzach.
Ten artykuł jest przygnębiający
Sytuacja jest przygnębiająca i powinnaś czuć się przygnębiona. Nie ma jeszcze niepowtarzalnego, deklaratywnego sposobu na szybkie i asynkronicznie pobieranie skryptów przy jednoczesnym kontrolowaniu kolejności ich wykonywania. Dzięki HTTP2/SPDY możesz zmniejszyć narzut żądania do tego stopnia, że dostarczanie skryptów w kilku małych plikach, które można buforować osobno, może być najszybszym sposobem. Wyobraź sobie:
<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>
Każdy skrypt rozszerzenia obsługuje określony komponent strony, ale wymaga funkcji pomocniczych w pliku dependencies.js. W idealnej sytuacji chcemy pobrać wszystkie elementy asynchronicznie, a potem jak najszybciej wykonać skrypty ulepszeń w dowolnej kolejności, ale po dependencies.js. To progresywne ulepszanie progresywnego ulepszania! Niestety nie ma deklaratywnego sposobu na osiągnięcie tego celu, chyba że skrypty zostaną zmodyfikowane w celu śledzenia stanu ładowania pliku dependencies.js. Nawet ustawienie async=false nie rozwiąże tego problemu, ponieważ wykonanie enhancement-10.js zostanie zablokowane na 1–9. W istocie jest tylko jedna przeglądarka, która umożliwia to bez konieczności stosowania obejść.
IE ma pomysł
IE wczytuje skrypty inaczej niż inne przeglądarki.
var script = document.createElement('script');
script.src = 'whatever.js';
IE zaczyna pobierać „whatever.js”, inne przeglądarki nie zaczynają pobierania, dopóki skrypt nie zostanie dodany do dokumentu. IE ma też zdarzenie „readystatechange” i właściwość „readystate”, które informują nas o postępie wczytywania. Jest to bardzo przydatne, ponieważ pozwala nam niezależnie kontrolować wczytywanie i wykonywanie skryptów.
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';
Możemy tworzyć złożone modele zależności, wybierając, kiedy dodawać skrypty do dokumentu. IE obsługuje ten model od wersji 6. To ciekawe, ale nadal występuje w nim ten sam problem z wykrywalnością w pre-loaderze, co w przypadku async=false
.
Wystarczy! Jak wczytywać skrypty?
Ok, ok. Jeśli chcesz wczytywać skrypty w sposób, który nie blokuje renderowania, nie wymaga powtarzania i jest doskonale obsługiwany przez przeglądarki, proponuję:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
To. Na końcu elementu body. Tak, bycie programistą stron internetowych jest bardzo podobne do bycia królem Syzyfem (bum! 100 punktów za nawiązanie do mitologii greckiej. Ograniczenia w HTML i przeglądarkach uniemożliwiają nam znaczne polepszenie jakości.
Mam nadzieję, że moduły JavaScript uratują nas, zapewniając deklaratywny, niezablokowany sposób wczytywania skryptów i kontrolę nad kolejnością ich wykonywania, choć wymaga to zapisania skryptów w postaci modułów.
Musi być coś lepszego, czego możemy teraz użyć.
Jeśli chcesz uzyskać naprawdę wysoką skuteczność i nie przeszkadza Ci nieco skomplikowana i powtarzalna konfiguracja, możesz połączyć kilka powyższych sztuczek.
Najpierw dodajemy deklarację zasobu podrzędnego dla wstępnego ładowania:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Następnie w głównej części dokumentu wczytujemy skrypty za pomocą JavaScriptu, używając tagu async=false
, a w razie potrzeby wczytywania skryptu na podstawie stanu gotowości IE lub opóźnionego wczytywania.
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>');
}
}
Po kilku trikach i skompresowaniu pliki skryptu zajmują 362 bajty plus adresy URL skryptu:
!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"
])
Czy warto dodać dodatkowe bajty w porównaniu z prostym includem skryptu? Jeśli do warunkowego wczytywania skryptów używasz już JavaScriptu, jak BBC, możesz też skorzystać z wcześniejszego uruchamiania tych pobrań. W przeciwnym razie pozostań przy prostej metodzie na końcu treści.
Teraz już wiem, dlaczego sekcja wczytywania skryptu WHATWG jest tak obszerna. Potrzebuję drinka.
Krótkie omówienie
Elementy zwykłego skryptu
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Specyfikacja: pobierz razem, wykonaj w kolejności po wszystkich oczekujących usługach, zablokuj renderowanie do czasu zakończenia. Przeglądarki: tak, proszę.
Odroczenie
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Specyfikacja: pobierz razem, wykonaj w kolejności tuż przed zdarzeniem DOMContentLoaded. Ignoruj „opóźnienie” w przypadku skryptów bez atrybutu „src”. IE < 10 mówi: mogę wykonać 2.js w połowie wykonania 1.js. Czy to nie fajne? W przeglądarkach oznaczonych na czerwono: nie mam pojęcia, co to jest „odłóż”, więc wczytam skrypty tak, jakby nie było tej opcji. Inne przeglądarki: ok, ale nie mogę zignorować „defer” w przypadku skryptów bez atrybutu „src”.
Dane asynchroniczne
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
Specyfikacja: pobieranie razem, wykonywanie w dowolnej kolejności. W przeglądarkach oznaczonych na czerwono: co to jest „async”? Załaduję skrypty tak, jakby nie było tego parametru. Inne przeglądarki: tak, ok.
Async false
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
Specyfikacja: pobieranie razem, wykonywanie w kolejności, gdy tylko wszystko zostanie pobrane. Firefox < 3.6, Opera: nie mam pojęcia, czym jest ta „asynchroniczność”, ale tak się składa, że skrypty dodane za pomocą JS wykonuję w kolejności, w jakiej zostały dodane. Safari 5.0: rozumiem „async”, ale nie rozumiem, jak ustawić go na „false” w JS. Skrypty będą wykonywane w dowolnej kolejności, gdy tylko się pojawią. IE < 10: nie wiem, o czym jest mowa, ale istnieje obejście za pomocą funkcji „onreadystatechange”. Inne przeglądarki w czerwonym kolorze: nie rozumiem tego „asyncjonalnego”, więc wykonam Twoje skrypty w dowolnej kolejności, gdy tylko się pojawią. Wszystkie inne odpowiedzi: „Jestem Twoim przyjacielem, zrobimy to zgodnie z zasadami”.