Komut dosyası yüklemenin belirsiz sularında derinlemesine inceleme

Jake Archibald
Jake Archibald

Giriş

Bu makalede, tarayıcıya nasıl JavaScript yükleyeceğinizi ve nasıl çalıştıracağınızı öğreneceksiniz.

Hayır, bekleyin, geri dönün. Bu işlemin basit ve sıradan gibi göründüğünün farkındayız. Ancak bu işlemin, teorik olarak basit olanın eski sürümlere dayalı bir tuhaflık çukuruna dönüştüğü tarayıcıda gerçekleştiğini unutmayın. Bu farklılıkları bilmek, komut dosyalarını yüklemenin en hızlı ve en az kesinti veren yolunu seçmenize olanak tanır. Zamanınız kısıtlıysa hızlı referans bölümüne atlayın.

Öncelikle, özelliğin bir komut dosyasının indirilip yürütülebileceği çeşitli yolları nasıl tanımladığını aşağıda bulabilirsiniz:

Komut dosyası yüklemeyle ilgili whatWG
Komut dosyası yüklemeyle ilgili WHATWG

Tüm WHATWG spesifikasyonları gibi, başlangıçta bir Scrabble fabrikasında gerçekleşen bir bomba saldırısının ardından ortaya çıkan manzara gibi görünür. Ancak 5. kez okuyup gözünüzdeki kanları sildikten sonra aslında oldukça ilginç olduğunu göreceksiniz:

İlk komut dosyam

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Ah, ne kadar basit ve neşeli. Bu durumda tarayıcı, her iki komut dosyasını da paralel olarak indirir ve sıralarını koruyarak en kısa sürede yürütür. "2.js", "1.js" çalıştırılana (veya çalıştırılamayana) kadar çalıştırılmaz. "1.js", önceki komut dosyası veya stil sayfası çalıştırılana kadar çalıştırılmaz.

Maalesef tarayıcı, tüm bunlar gerçekleşirken sayfanın daha fazla oluşturulmasını engeller. Bunun nedeni, "web'in ilk çağı"ndan kalma DOM API'leridir. Bu API'ler, ayrıştırıcının işlediği içeriğe dizelerin eklenmesine olanak tanır (ör. document.write). Yeni tarayıcılar, dokümanı arka planda taramaya veya ayrıştırmaya devam edecek ve ihtiyaç duyabileceği harici içerik (js, resimler, css vb.) için indirme işlemlerini tetikleyecektir. Ancak oluşturma işlemi yine de engellenmiştir.

Performans dünyasının en iyi ve iyi yanları, mümkün olduğunca az içeriğin görünmesini engellediği için belgenizin sonuna komut dosyası öğeleri eklemenizi önerir. Maalesef bu, tüm HTML'niz indirilene kadar komut dosyanızın tarayıcı tarafından görülmediği anlamına gelir. Bu noktada tarayıcı, CSS, resimler ve iFrame'ler gibi diğer içerikleri indirmeye başlar. Modern tarayıcılar, görüntülere göre JavaScript'e öncelik verecek kadar akıllıdır ancak daha iyisini yapabiliriz.

Teşekkür ederiz IE. (Hayır, alay etmiyorum.)

<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>

Microsoft bu performans sorunlarını fark etti ve Internet Explorer 4'e "erteleme" özelliğini ekledi. Bu, temel olarak şunu ifade eder: "document.write gibi şeyler kullanarak ayrıştırıcıya içerik yerleştirmeyeceğime söz veriyorum. Bu sözü yerine getirirsem beni uygun gördüğünüz herhangi bir şekilde cezalandırabilirsiniz." şeklinde bir açıklama içeriyor. Bu özellik HTML4'e dönüştürüldü ve diğer tarayıcılarda da gösterildi.

Yukarıdaki örnekte tarayıcı, her iki komut dosyasını da paralel olarak indirir ve sıralarını koruyarak DOMContentLoaded tetiklenmeden hemen önce yürütür.

Tıpkı koyun fabrikasındaki küme bomba gibi “erteleme” de yünlü bir karmaşaya dönüştü. "src" ve "defer" özellikleri ile komut dosyası etiketleri ve dinamik olarak eklenen komut dosyaları arasında 6 komut dosyası ekleme yöntemi vardır. Elbette, tarayıcılar uygulanmaları gereken sıra üzerinde anlaşmamışlardı. Mozilla, 2009'daki durumu ele alan harika bir makale yazdı.

NEWG bu davranışı açıkça belirterek "ertele"nin dinamik olarak eklenen veya "src" içermeyen komut dosyaları üzerinde herhangi bir etkisinin olmadığını beyan etti. Aksi takdirde, ertelenen komut dosyaları, doküman ayrıştırıldıktan sonra, eklendikleri sırayla çalıştırılmalıdır.

Teşekkür ederiz IE. (Tamam, şimdi alaycı bir üslupla konuşuyorum.)

Hem verir hem de alır. Maalesef IE4-9'da komut dosyalarının beklenmedik bir sırada yürütülmesine neden olabilecek kötü bir hata var. Süreç şu şekilde işler:

1.js

console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');

2.js

console.log('3');

Sayfada bir paragraf olduğu varsayıldığında, beklenen günlük sırası [1, 2, 3] şeklindedir. Ancak, IE9 ve altında [1, 3, 2] sonucunu görürsünüz. Belirli DOM işlemleri, IE'nin mevcut komut dosyası yürütülmesini duraklatmasına ve devam etmeden önce diğer bekleyen komut dosyalarını yürütmesine neden olur.

Ancak IE10 ve diğer tarayıcılar gibi hatasız uygulamalarda bile komut dosyası yürütme, dokümanın tamamı indirilip ayrıştırılana kadar geciktirir. DOMContentLoaded için zaten bekleyecekseniz bu yöntemi kullanabilirsiniz. Ancak performans konusunda gerçekten agresif olmak istiyorsanız dinleyici eklemeye ve daha erken kendi kendine başlatmaya başlayabilirsiniz…

Yardımınız için HTML5

<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>

HTML5 bize document.write kullanmayacağınızı varsaymasına rağmen belgenin ayrıştırılmasını beklemeden yürüten yeni bir özellik olan "async"i sundu. Tarayıcı her iki komut dosyasını paralel olarak indirecek ve bunları mümkün olan en kısa zamanda yürütecektir.

Maalesef, mümkün olan en kısa sürede yürütülecekleri için "2.js", "1.js"den önce yürütülebilir. Bağımsızlarsa bu sorun olmaz. Belki de "1.js", "2.js" ile hiçbir ilgisi olmayan bir izleme komut dosyasıdır. Ancak "1.js", "2.js"nin bağlı olduğu jQuery'nin bir CDN kopyasıysa sayfanız, bir… bilmiyorum… bunun için aklıma bir şey gelmiyor.

İhtiyacımız olan şey bir JavaScript kitaplığı.

Buradaki amaç, bir dizi komut dosyasını, oluşturmayı engellemeden hemen indirip eklendikleri sırayla çalıştırmaktır. Maalesef HTML sizden nefret ediyor ve bunu yapmanıza izin vermiyor.

Bu sorun, JavaScript tarafından birkaç farklı şekilde çözüldü. Bazıları, JavaScript'inizde değişiklik yapmanızı ve kitaplığın doğru sırada çağırdığı bir geri çağırma işlevine (ör. RequireJS) sarmalamanızı gerektiriyordu. Diğerleri, paralel olarak indirmek için XHR'yi, ardından doğru sırada eval()'yi kullanırdı. Bu yöntem, CORS başlığı olmadığı ve tarayıcı tarafından desteklenmediği sürece başka bir alandaki komut dosyalarında çalışmazdı. Bazıları LabJS gibi süper sihirli kodlar bile kullandı.

Bu saldırılar, tamamlandığında bir etkinliği tetikleyecek şekilde kaynağı indirmesi için tarayıcıyı kandırmayı içeriyordu ancak bunu yürütmekten kaçınıyordu. LabJS'de komut dosyası, yanlış bir mime türüyle (ör. <script type="script/cache" src="...">) eklenir. Tüm komut dosyaları indirildikten sonra, tarayıcının bunları doğrudan önbellekten alıp sırayla yürütmesini umarak doğru türle tekrar eklenirdi. Bu, kullanışlı ancak belirtilmemiş bir davranışa bağlıydı ve HTML5'te tarayıcıların tanınmayan türde komut dosyaları indirmemesi gerektiği belirtildiğinde bozuldu. LabJS'nin bu değişikliklere uyum sağladığını ve artık bu makaledeki yöntemlerin bir kombinasyonunu kullandığını belirtmek isteriz.

Ancak, komut dosyası yükleyicilerin kendi performans sorunları vardır. Kitaplığın yönettiği komut dosyaları indirilmeden önce JavaScript'in indirilmesini ve ayrıştırılmasını beklemeniz gerekir. Ayrıca komut dosyası yükleyiciyi nasıl yükleyeceğiz? Komut dosyası yükleyicisine ne yüklemesi gerektiğini söyleyen komut dosyasını nasıl yükleyeceğiz? Kimler izleyicileri izliyor? Neden çıplak görünüyorum? Bunların hepsi zor sorular.

Temelde, diğer komut dosyalarını indirmeyi düşünmeden önce ekstra bir komut dosyası dosyası indirmeniz gerekiyorsa performans savaşını kaybettiğiniz anlamına gelir.

DOM'un yardımı

Yanıt aslında HTML5 spesifikasyonundadır ancak komut dosyası yükleme bölümünün en altında gizlidir.

Bunu "Dünyalı" olarak çevirelim:

[
  '//other-domain.com/1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  document.head.appendChild(script);
});

Dinamik olarak oluşturulan ve dokümana eklenen komut dosyaları varsayılan olarak eşzamansızdır. Bu komut dosyaları, oluşturmayı engellemez ve indirildikten hemen sonra yürütülür. Bu nedenle, yanlış sırada görünebilirler. Ancak bunları açıkça eşzamanlı olmayan olarak işaretleyebiliriz:

[
  '//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);
});

Bu, komut dosyalarımıza düz HTML ile gerçekleştirilemeyen davranışların bir karışımını sağlar. Açıkça asynkron olmadıkları için komut dosyaları, ilk düz HTML örneğimizde eklendikleri sıraya eklenir. Ancak dinamik olarak oluşturuldukları için belge ayrıştırma işleminin dışında çalıştırılırlar. Bu nedenle, indirilirken oluşturma işlemi engellenmez (asenkron olmayan komut dosyası yüklemeyi, hiçbir zaman iyi bir şey olmayan senkron XHR ile karıştırmayın).

Yukarıdaki komut dosyası, sayfaların başına satır içi olarak eklenmelidir. Bu komut dosyası, aşamalı oluşturmayı kesintiye uğratmadan komut dosyası indirme işlemlerini en kısa sürede sıraya koyar ve belirttiğiniz sırada en kısa sürede yürütür. "2.js", "1.js"den önce ücretsiz olarak indirilebilir ancak "1.js" başarıyla indirilip çalıştırılana ya da bunlardan biri başarısız olana kadar yürütülmez. Yaşasın! Asynk-download ancak ordered-execution!

Komut dosyalarının bu şekilde yüklenmesi, Safari 5.0 hariç async özelliğini destekleyen tüm tarayıcılarda desteklenir (5.1 desteklenir). Ayrıca, Firefox ve Opera'nın tüm sürümleri desteklenir. Çünkü bu sürümler, async özelliğini desteklemez ve dinamik olarak eklenen komut dosyalarını dokümana eklendikleri sırayla yürütür.

Komut dosyalarını yüklemenin en hızlı yolu bu, değil mi? Evet.

Hangi komut dosyalarının yükleneceğine dinamik olarak karar veriyorsanız evet, aksi takdirde muhtemelen hayır. Yukarıdaki örnekte, tarayıcının hangi komut dosyalarının indirileceğini bulmak için komut dosyasını ayrıştırması ve yürütmesi gerekir. Bu şekilde komut dosyalarınız önceden yükleme tarayıcılarından gizlenmiş olur. Tarayıcılar, bir sonraki ziyaret edeceğiniz sayfalardaki kaynakları keşfetmek veya ayrıştırıcı başka bir kaynak tarafından engellenirken sayfa kaynaklarını keşfetmek için bu tarayıcıları kullanır.

Aşağıdaki kodu dokümanın başına ekleyerek bulunabilirliği tekrar ekleyebiliriz:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Bu, tarayıcıya sayfanın 1.js ve 2.js dosyalarına ihtiyacı olduğunu bildirir. link[rel=subresource], link[rel=prefetch]'e benzer ancak farklı anlamlara sahiptir. Ne yazık ki bu özellik şu anda yalnızca Chrome'da destekleniyor. Hangi komut dosyalarının yükleneceğini iki kez belirtmeniz gerekir: bir kez bağlantı öğeleri aracılığıyla, bir kez de komut dosyanızda.

Düzeltme: Bu öğelerin önceden yükleme tarayıcı tarafından alındığını belirtmiştik. Ancak bu öğeler normal ayrıştırıcı tarafından alınır. Ancak ön yükleme tarayıcı bu komut dosyalarını algılayabilir, ancak henüz bunu yapmıyor. Öte yandan, yürütülebilir kod tarafından dahil edilen komut dosyaları hiçbir zaman ön yüklenemez. Yorumlarda beni düzelten Yoav Weiss'e teşekkür ederim.

Bu makale beni üzüyor

İçinde bulunduğunuz durum moral bozucu ve depresyonda olmalısınız. Yürütme sırasını kontrol ederken komut dosyalarını hızlı ve eşzamansız olarak indirmenin yinelemeli olmayan ancak bildirimli bir yolu yoktur. HTTP2/SPDY ile istek yükü, komut dosyalarını tek tek önbelleğe alınabilen birden fazla küçük dosyada yayınlamanın en hızlı yol olabileceği noktaya kadar azaltılabilir. Şunu düşünün:

<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>

Her geliştirme komut dosyası belirli bir sayfa bileşeniyle ilgilenir, ancakdonencies.js içinde yardımcı işlev işlevleri gerektirir. İdeal olarak, tüm eşzamansız olarak indirmek ve ardından geliştirme komut dosyalarını mümkün olan en kısa sürede, herhangi bir sırayla ancak additionalencies.js'den sonra yürütmek isteriz. Bu bir ilerleme, progresif bir geliştirme çalışmasıdır. Maalesef komut dosyalarının kendisi dependencies.js dosyasının yükleme durumunu izleyecek şekilde değiştirilmediği sürece bunu yapmanın açık bir yolu yoktur. enhancement-10.js dosyasının yürütülmesi 1-9 arasında engelleneceğinden, async=false bile bu sorunu çözmez. Aslında, bu işlemi herhangi bir hile olmadan mümkün kılan tek bir tarayıcı var…

IE'nin bir fikri var.

IE, komut dosyalarını diğer tarayıcılardan farklı şekilde yükler.

var script = document.createElement('script');
script.src = 'whatever.js';

IE, "whatever.js" dosyasını indirmeye başlar. Diğer tarayıcılar, komut dosyası dokümana eklenene kadar indirme işlemini başlatmaz. IE'de, yükleme ilerleme durumunu bize bildiren bir "readystatechange" etkinliği ve "readystate" özelliği de vardır. Bu, komut dosyalarının yüklenmesini ve yürütülmesini bağımsız olarak kontrol etmemize olanak tanıdığı için gerçekten çok kullanışlıdır.

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';

Dokümana ne zaman komut dosyası ekleneceğini seçerek karmaşık bağımlılık modelleri oluşturabiliriz. IE, 6 sürümünden itibaren bu modeli desteklemektedir. Oldukça ilginç olsa da, async=false ile aynı ön yükleyici keşfedilebilirliği sorunu yaşıyor.

Yeter! Komut dosyalarını nasıl yüklemeliyim?

Tamam. Komut dosyalarını oluşturmayı engellemeyecek, tekrar içermeyecek ve mükemmel tarayıcı desteği sunacak şekilde yüklemek istiyorsanız önerdiğimiz yöntemi aşağıda bulabilirsiniz:

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Bu. body öğesinin sonunda. Evet, web geliştirici olmak Sisifos'a (boom! Yunan mitolojisi referansı için 100 hipster puanı!). HTML ve tarayıcılardaki sınırlamalar, çok daha iyi sonuçlar elde etmemizi engelliyor.

JavaScript modülleri, komut dosyalarını yüklemek ve yürütme sırası üzerinde kontrol sağlamak için beyanda bulunarak ve engellemeyen bir yol sunarak bizi kurtaracaktır. Bunun için komut dosyalarının modül olarak yazılması gerekir.

Eww, şu anda kullanabileceğimiz daha iyi bir şey olmalı.

Anlaşıldı. Performans konusunda gerçekten agresif olmak istiyorsanız ve biraz karmaşıklık ve tekrardan rahatsız olmuyorsanız yukarıdaki ipuçlarının birkaçını birleştirebilirsiniz.

Öncelikle, ön yükleyiciler için alt kaynak beyanını ekleriz:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Ardından, belgenin başlığında satır içi olarak komut dosyalarımızı JavaScript ile yükleriz. Bu işlem için async=false kullanırız. IE'nin hazır duruma dayalı komut dosyası yükleme özelliğine ve ardından ertelemeye geri döneriz.

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>');
  }
}

Birkaç hile ve sıkıştırma işleminden sonra, komut dosyası 362 bayt ve komut dosyası URL'lerinizden oluşur:

!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"
])

Basit bir komut dosyası dahil etmeye kıyasla ek baytlara değer mi? Komut dosyalarını koşullu olarak yüklemek için zaten JavaScript kullanıyorsanız (BBC gibi) bu indirmeleri daha erken tetiklemekten de yararlanabilirsiniz. Belki de hayır, sadece vücudun sonunu getirmeyi seçebilirsiniz.

WHATWG komut dosyası yükleme bölümünün neden bu kadar geniş olduğunu anladım. Bir şeyler içmem lazım.

Hızlı referans

Düz komut dosyası öğeleri

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Spesifikasyonda: Birlikte indirin, bekleyen CSS'lerden sonra sırayla yürütün, tamamlanana kadar oluşturmayı engelleyin. Tarayıcılar: Evet efendim.

Ertele

<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>

Özellikler şöyledir: Birlikte indirin, DOMContentLoaded'dan hemen önceki sırayla yürütün. "src" içermeyen komut dosyalarında "ertele"yi yoksayın. IE < 10 şunu söylüyor: 2.js'yi, 1.js'nin yürütülmesinin ortasında çalıştırabilirim. Bu eğlenceli değil mi? Kırmızı renkli tarayıcılar şöyle diyor: "Erteleme" sorununun ne olduğu konusunda hiçbir fikrim yok, komut dosyalarını orada yokmuş gibi yükleyeceğim. Diğer tarayıcılar: Tamam, ancak "src" olmadan komut dosyalarında "defer"i yoksaymayabilirim.

Asenk.

<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>

Özellik: Birlikte indirin, indirildikleri sırayla yürütün. Kırmızı renkli tarayıcılar: "async" nedir? Komut dosyalarını bu özellik yokmuş gibi yükleyeceğim. Diğer tarayıcılar şöyle diyor: Evet.

Eş zamansız yanlış

[
  '1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

Spesifikasyonda: Birlikte indirin, tüm indirme işlemi tamamlanır tamamlanmaz sırayla yürütün. Firefox < 3.6, Opera: Bu "async" şeyin ne olduğundan haberim yok ama JS aracılığıyla eklenen komut dosyalarını eklendikleri sırayla yürütüyorum. Safari 5.0'ta: "async" ifadesini anlıyorum ancak JS ile "false" olarak ayarlanmasını anlamıyorum. Komut dosyalarınızı bize ulaşır ulaşmaz hangi sırayla yürüteceğim. IE 10'dan eski sürümler: "async" hakkında hiçbir fikrimiz yok ancak "onreadystatechange" kullanılarak bu sorunun geçici bir çözümü var. Diğer kırmızı renkli tarayıcılar: Bu "async" işini anlamıyoruz. Komut dosyalarınızı, geldikleri anda herhangi bir sırayla yürüteceğiz. Diğer tüm mesajlar: Ben sizin dostunuzum, bu işi kurallara uygun şekilde yapacağız.