Wprowadzenie
Z tego artykułu dowiesz się, jak wczytać i uruchomić 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ę dziwną pozostałością po starszych wersjach. 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 Przewodnik.
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 gier słownych, ale gdy przeczytasz to już 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ść. W tym przypadku przeglądarka pobierze oba skrypty równolegle i wykona je tak szybko, jak to możliwe, zachowując ich kolejność. Kod „2.js” nie zostanie wykonany, dopóki skrypt „1.js” nie zostanie wykonany (lub nie zostanie wykonany), „1.js” nie zostanie wykonany do czasu wykonania poprzedniego skryptu lub arkusza stylów 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 parsowanie, np. document.write
. Nowsze przeglądarki będą nadal skanować lub analizować dokument w tle i uruchamiać pobieranie treści zewnętrznych, których mogą potrzebować (js, obrazy, css itp.), ale renderowanie nadal będzie zablokowane.
Dlatego eksperci od wydajności zalecają umieszczanie elementów skryptu na końcu dokumentu, ponieważ w ten sposób blokuje się jak najmniej treści. Niestety oznacza to, że skrypt nie jest widoczny dla przeglądarki, dopóki nie pobierze całego kodu HTML i rozpoczyna pobieranie innych treści, np. CSS, obrazów i elementów iframe. Nowoczesne przeglądarki są na tyle inteligentne, że priorytetowo traktują JavaScript zamiast obrazów, ale możemy to jeszcze poprawić.
Dzięki, 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 w Internet Explorerze 4 funkcję „opóźnianie”. 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ę”. 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 IE 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 zwiększeniu 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 zależy plikowi „2.js”, Twoja strona zostanie pokryta błędami, jak ciastko w … Nie mam pojęcia, co tu wpisać.
Wiem, czego potrzebujemy, biblioteki JavaScriptu!
Święty Graal to natychmiastowe pobieranie zestawu skryptów bez blokowania renderowania i jak najszybsze ich wykonanie w kolejności dodania. Kod HTML Cię nienawidzi i nie możesz Ci na to pozwolić.
Skrypt JavaScript rozwiązał ten problem w kilku wersjach. Niektóre z nich wymagały wprowadzenia zmian w pliku JavaScript, np. umieszczenia go w funkcji zwracającej wartość, którą biblioteka wywołuje w prawidłowej kolejności (np. RequireJS). Inni używają XHR do pobierania równoległego, a następnie eval()
w prawidłowej kolejności. Nie działało to w przypadku skryptów w innej domenie, chyba że miał nagłówek CORS, a przeglądarka go 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 będą mogły się pobrać, musisz poczekać, aż biblioteka JavaScript zostanie pobrana i przeanalizowana. Jak też załadujemy skrypt? Jak wczytać skrypt, który mówi ładowarce, co ma wczytać? Kto obserwuje Strażników? Dlaczego jestem nagi? To wszystkie trudne pytania.
Jeśli musisz pobrać dodatkowy plik skryptu, zanim zaczniesz myśleć o pobieraniu innych skryptów, już na starcie przegrywasz walkę o wydajność.
DOM na ratunek
Odpowiedź kryje się w specyfikacji HTML5, ale u dołu sekcji ładowania skryptów jest ukryta.
Tłumaczenie na „Ziemianin”:
[
'//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łównej części nagłówka strony, aby jak najszybciej umieścić w kole skrypty do pobrania bez zakłócania progresywnego renderowania i jak najszybciej je 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 się tego zrobić. 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 przeglądarek Firefox i Opera, 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ępnie wczytywanych stron. 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 blokowane 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. Nazwa link[rel=subresource]
jest podobna do link[rel=prefetch]
, ale ma inną semantykę. Jest ona obecnie obsługiwana tylko w Chrome i musisz zadeklarować, które skrypty mają się wczytywać dwa razy: raz za pomocą elementów linku, a potem w skrypcie.
Poprawka: pierwotnie stwierdziliśmy, że te błędy były wykrywane przez skaner wstępnego wczytania, ale tak nie jest. Wykrywał je zwykły parsujący. Skaner wstępnego wczytywania może je jednak uwzględnić, ale jeszcze tego nie zrobił. Skrypty zawarte w edytowalnym kodzie nie mogą być wstępnie wczytane. Dziękuję Yoav Weiss, który poprawił mnie w komentarzach.
Ten artykuł jest przygnębiający
Sytuacja jest przygnębiająca i powinnaś czuć się przygnębiona. Nie ma jeszcze deklaratywnego sposobu na szybkie i asyncjoniczne pobieranie skryptów przy jednoczesnym kontrolowaniu kolejności ich wykonywania. Protokół HTTP2/SPDY zmniejszają obciążenie związane z żądaniami do tego stopnia, że najszybszym sposobem na przesłanie skryptów w wielu niewielkich plikach, które można samodzielnie zapisywać w pamięci podręcznej, jest przesyłanie skryptów. 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 zajmuje się określonym komponentem strony, ale wymaga funkcji pomocniczych w plikach dependencies.js. Najlepiej pobrać wszystko asynchronicznie, a potem jak najszybciej wykonać skrypty ulepszeń, w dowolnej kolejności, ale po zakończeniu pliku Recommended.js. To stopniowe ulepszenie. Niestety nie ma deklaratywnego sposobu na osiągnięcie tego celu, chyba że skrypty zostaną zmodyfikowane, aby śledzić stan ł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 rozpocznie pobieranie pliku „whatever.js”. Inne przeglądarki zaczną pobierać plik dopiero po dodaniu skryptu do dokumentu. IE ma też zdarzenie „readystatechange” i właściwość „readystate”, które informują nas o postępie wczytywania. To bardzo przydatne, bo pozwala nam kontrolować wczytywanie i wykonywanie skryptów niezależnie.
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 poprzednim ładowaniu co w przypadku async=false
.
Wystarczy! Jak wczytywać skrypty?
Dobra, dobra. Jeśli chcesz wczytywać skrypty w sposób, który nie blokuje renderowania, nie wymaga powtarzania i jest doskonale obsługiwany przez przeglądarkę, proponuję:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
To. Na końcu elementu body. Tak, bycie twórcą stron internetowych jest jak Król Syzyf (doskonale! 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 nas uratują, zapewniając deklaratywny, nieblokujący sposób ładowania skryptów i zapewniający kontrolę nad kolejnością wykonywania (choć wymaga to napisania skryptów w postaci modułów).
Musi być coś lepszego, czego możemy 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 nagłówku dokumentu, wczytujemy nasze skrypty za pomocą kodu JavaScript, używając async=false
, w zależności od sposobu ładowania skryptów w IE i z opóźnieniem.
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 rozmiar skryptu to 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ń. Jeśli nie, użyj prostej metody umieszczenia linku 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 „defer” 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, o co chodzi w tym „opóźnieniu”, więc wczytam skrypty tak, jakby nie było tego opóźnienia. 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.
Asynchroniczna wartość fałsz
[
'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 kolorze czerwonym: nie rozumiem tego „async”, więc wykonam Twoje skrypty w dowolnej kolejności, gdy tylko się pojawią. Wszystkie inne odpowiedzi: „Jestem Twoim przyjacielem, zrobimy to zgodnie z zasadami”.