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

Jake Archibald
Jake Archibald

Giriş

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

Hayır, bekle, geri dön! Kulağa sıradan ve basit gelebilir, ancak bunun sadece tarayıcı üzerinden yaşandığını ve teorik olarak basitliğin eskiden beri tuhaf bir durum haline geldiğini unutmayın. Bu özellikleri bilmek, komut dosyalarını en hızlı ve en az kesintiye uğratacak yolu seçmenizi sağlar. Programınız kısıtlıysa hızlı başvuru bölümüne geçin.

Öncelikle bu spesifikasyonun bir komut dosyasının indirilip çalıştırılabileceği çeşitli yolları nasıl tanımladığı aşağıda açıklanmıştır:

Komut dosyası yüklenirken WHATWG
Komut dosyası yüklenirken WHATWG'si

Tüm WHATWG spesifikasyonlarında olduğu gibi, ilk başta toprak bombası olarak sürülen bir fabrikada oluşan bombanın sonucu gibi görünüyor. Ancak 5. kez okuduktan sonra gözlerinizden kanları sildikten sonra ortaya gerçekten çok ilgi çekici geliyor:

İlk komut dosyam şunları içeriyor:

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

Muhteşem sadelik. Burada tarayıcı her iki komut dosyasını da paralel olarak indirir ve sıralarını koruyarak mümkün olan en kısa sürede yürütür. "2.js", "1.js" çalıştırılana (veya yapılamayana) kadar yürütülmez; "1.js", önceki komut dosyası veya stil sayfası yürütülene kadar yürütülmez vb.

Maalesef tüm bunlar yaşanırken tarayıcı sayfanın daha fazla oluşturulmasını engeller. Bunun nedeni, ayrıştırıcının çiğnediği içeriğe dizelerin eklenmesine olanak tanıyan document.write gibi "web'in ilk çağındaki" DOM API'leridir. Yeni tarayıcılar dokümanı arka planda taramaya veya ayrıştırmaya devam eder ve ihtiyaç duyabileceği harici içeriğin (js, resimler, css vb.) indirilmesini tetikler ancak oluşturma yine de engellenir.

Bu nedenle, komut dosyası öğeleri mümkün olduğunca az içeriği engelleyeceğinden, dokümanınızın sonuna komut dosyası öğeleri yerleştirmenizi tavsiye ederiz. Ne yazık ki bu, komut dosyanızın tüm HTML'nizi indirmeden tarayıcı tarafından görülmediği ve bu noktada CSS, resimler ve iframe'ler gibi diğer içerikleri indirmeye başladığı anlamına gelir. Modern tarayıcılar, görüntülerden çok JavaScript'e öncelik verecek kadar akıllıdır, ancak biz daha iyisini yapabiliriz.

Teşekkürler IE! (hayır, alaycı yapmıyorum)

<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'te "ertele" özelliğini kullanıma sundu. Bu temel olarak şunu söyler: “document.write gibi şeyleri kullanarak ayrıştırıcıya öğe eklememeye söz veriyorum. Bu sözü yerine getirmezsem beni uygun gördüğünüz herhangi bir şekilde cezalandırabilirsiniz.”. Bu özellik, HTML4'e dönüştürüldü ve diğer tarayıcılarda göründü.

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

Koyun fabrikasındaki bir bomba gibi, "ertele" seçeneği de korkunç bir karmaşaya dönüştü. "src" ile "erte" özellikleri ve komut dosyası etiketleri ile dinamik olarak eklenen komut dosyaları arasında, komut dosyası eklemek için 6 kalıbımız vardır. Elbette tarayıcılar, yürütme sırası üzerinde anlaşmaya varmadı. 2009 yılında uğradığı tarihte Mozilla bu sorunla ilgili harika bir yazı yazdı.

WHATWG, davranışı açık bir hale getirerek dinamik olarak eklenen veya "src" içermeyen komut dosyaları üzerinde "ertele" işlevinin herhangi bir etkisi olmadığını belirtti. Aksi takdirde, ertelenen komut dosyalarının, doküman ayrıştırıldıktan sonra, eklenme sırasına göre çalışması gerekir.

Teşekkürler IE! (tamam, şimdi iğneleyici davranıyorum)

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 vardır. 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ğunu varsayarsak, günlüklerin beklenen sırası [1, 2, 3] olur, ancak IE9 ve altlarında [1, 3, 2] değerleri gösterilir. Belirli DOM işlemleri, IE'nin devam etmeden önce mevcut komut dosyası yürütmesini duraklatmasına ve beklemedeki diğer komut dosyalarını yürütmesine neden olur.

Ancak IE10 ve diğer tarayıcılar gibi hatalı olmayan uygulamalarda bile komut dosyasının yürütülmesi, dokümanın tamamı indirilip ayrıştırılana kadar gecikir. Yine de DOMContentLoaded için bekleyecekseniz bu özellik kullanışlı olabilir, ancak performans konusunda gerçekten agresif olmak istiyorsanız işleyici eklemeye ve önyüklemeye daha erken başlayabilirsiniz.

Kurtarıcınız HTML5

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

HTML5 bize, document.write kullanmayacağınızı varsayan ancak doküman yürütülmesi için ayrıştırılana kadar beklemeyen "async" adlı yeni bir özellik verdi. Tarayıcı, her iki komut dosyasını da paralel olarak indirir ve bunları mümkün olan en kısa sürede yürütür.

Maalesef mümkün olan en kısa sürede çalıştırılacakları için "2.js", "1.js"den önce yürütülebilir. Bağımsız olmaları halinde bu sorun olmaz, belki de "1.js" "2.js" ile ilgisi olmayan bir izleme komut dosyasıdır. Ancak "1.js"niz "2.js"nin bağlı olduğu jQuery'nin CDN kopyasıysa, sayfanız... I.js'nin hatalarla ilişkilendirilmesinden daha iyi sonuç vermez.

Neye ihtiyacımız olduğunu biliyorum, bir JavaScript kitaplığı!

Kutsal yöntem, oluşturmayı engellemeden bir dizi komut dosyasının hemen indirilmesini ve eklendikleri sırayla yürütülmesini sağlar. Ne yazık ki HTML sizden nefret ediyor ve bunu yapmanıza izin vermiyor.

Bu sorun JavaScript ile birkaç tatta çözüldü. Bazıları, JavaScript'inizi kitaplığın doğru sırayla çağırdığı bir geri çağırma için sarmalayarak (ör. RequireJS) değişiklikler yapmanızı gerektiriyordu. Bazıları ise paralel olarak indirmek için XHR kullanır, ardından doğru sırada eval() kullanır. CORS başlığı olmadığı ve tarayıcı bunu desteklemediği sürece başka bir alan adındaki komut dosyalarında çalışmaz. Hatta bazıları LabJS gibi süper sihirli hileler bile kullandı.

Saldırılar, tarayıcıdaki kaynağı, etkinlik tamamlandığında tetikleyecek şekilde indirmesi için kandırmayı içeriyordu, ancak onu çalıştırmaktan kaçınıyordu. LabJS'de, komut dosyası yanlış bir mime türüyle (ör. <script type="script/cache" src="...">) ekleniyordu. Tüm komut dosyaları indirildikten sonra, tarayıcının bunları doğrudan önbellekten alıp sırayla hemen yürüteceği umuduyla, doğru türde tekrar ekleniyorlardı. Uygun ancak belirtilmemiş bir davranışa bağlı olan bu durum, HTML5 tarafından belirtilen tarayıcıların tanınmayan bir türe sahip komut dosyalarını indirmemesi gerektiği zaman çöktü. LabJS'nin bu değişikliklere uyum sağladığını ve artık bu makaledeki yöntemlerin bir kombinasyonunu kullanmakta olduğunu belirtmek isteriz.

Bununla birlikte, komut dosyası yükleyicilerin kendi performans sorunları vardır. Yönettiği komut dosyalarının indirilmeye başlayabilmesi için kitaplığın JavaScript'inin indirilip ayrıştırılmasını beklemeniz gerekir. Ayrıca, komut dosyası yükleyicisini nasıl yükleyeceğiz? Komut dosyası yükleyicisine ne yüklemesi gerektiğini söyleyen komut dosyasını nasıl yükleyeceğiz? Bekçileri kim izliyor? Neden çıplakım? Bunların hepsi zor sorular.

Temel olarak, diğer komut dosyalarını indirmeyi düşünmeden fazladan bir komut dosyası indirmeniz gerekirse performans savaşını kaybetmiş olursunuz.

Kurtarıcınız DOM

Bu sorunun yanıtı aslında HTML5 spesifikasyonundadır, ancak komut dosyası yükleme bölümünün altında gizlenmiştir.

Bunu "Topraklama"ya çevirelim:

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

Dinamik bir şekilde oluşturulan ve dokümana eklenen komut dosyaları varsayılan olarak eş zamansızdır, oluşturmayı engellemez ve indirilir indirilmez yürütülür. Bu da yanlış sırada ortaya çıkabileceği anlamına gelir. Ancak, bunları açıkça eşzamansız değil şeklinde 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 yöntem, komut dosyalarımıza, düz HTML ile gerçekleştirilemeyen bir davranış karışımı sağlar. Komut dosyaları, açık bir şekilde eşzamansız olmadığından, ilk düz HTML örneğimizde eklendikleri sıra ile aynı yürütme sırasına eklenir. Bununla birlikte, dinamik olarak oluşturulduklarından doküman ayrıştırma dışında yürütülürler. Bu nedenle, indirilirken oluşturma işlemi engellenmez (eş zamansız komut dosyası yüklemeyi senkronize XHR ile karıştırmayın; bu hiçbir zaman iyi bir şey değildir).

Yukarıdaki komut dosyası, progresif oluşturmayı kesintiye uğratmadan komut dosyası indirmelerini mümkün olan en kısa sürede sıraya alarak sayfaların başına eklenmeli ve mümkün olan en kısa sürede yürütülmelidir. "2.js", "1.js" öncesinde ücretsiz olarak indirilebilir. Ancak "1.js" başarıyla indirilip çalıştırılıncaya veya ikisine de girene kadar yürütülmez. Yaşasın! Eş zamansız indirme ama sıralı yürütme!

Komut dosyalarının bu şekilde yüklenmesi, Safari 5.0 (5.1'de sorun oluşturmaz) hariç eş zamansız özelliği destekleyen her şey tarafından desteklenir. Ayrıca, eşzamansız özelliğini desteklemeyen sürümler olarak Firefox ve Opera'nın tüm sürümleri desteklenir. Bunlar, dinamik olarak eklenen komut dosyalarını, dokümana eklenmeleri sırayla kolayca yürütür.

Komut dosyalarını yüklemenin en hızlı yolu bu, değil mi? ya da sağa kaydırdığında ne olur?

Hangi komut dosyalarının yükleneceğine dinamik olarak karar veriyorsanız evet, aksi takdirde olmayabilir. Yukarıdaki örnekte, hangi komut dosyalarının indirileceğini keşfetmek için tarayıcının komut dosyasını ayrıştırması ve çalıştırması gerekir. Bu, komut dosyalarınızı önceden yükleme tarayıcılarından gizler. Tarayıcılar, daha sonra ziyaret etme olasılığınızın yüksek olduğu 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.

Bunu dokümanın başına koyarak keşfedilebilirliğ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 kodlarına ihtiyaç duyduğunu bildirir. link[rel=subresource], link[rel=prefetch] ile benzerdir ancak farklı anlamlara sahiptir. Maalesef şu anda yalnızca Chrome'da desteklenmektedir ve hangi komut dosyalarının bir kez bağlantı öğeleri aracılığıyla, sonra da komut dosyanızda iki kez yükleneceğini belirtmeniz gerekir.

Düzeltme: Başlangıçta bunların önyükleme tarayıcısı tarafından seçildiğini belirtmiştim, değiller, normal ayrıştırıcı tarafından toplanacaklar. Ancak, önceden yükleme tarayıcısı bunları alabilir, ancak henüz almaz. Bununla birlikte, yürütülebilir kod tarafından eklenen komut dosyaları hiçbir zaman önceden yüklenemez. Yorumlarda beni düzelten Yoav Weiss'a teşekkür ederim.

Bu makaleyi iç karartıcı buluyorum

Durumunuz moral bozucu ve moral bozucu hissediyorsunuz. Yürütme sırasını kontrol ederken komut dosyalarını hızlı ve eşzamansız olarak indirmenin yinelenmeyen ancak bildirim temelli bir yöntemi yoktur. HTTP2/SPDY ile istek ek yükünü, komut dosyalarını ayrı ayrı önbelleğe alınabilir birden fazla küçük dosyada iletmenin en hızlı yol olabileceği noktaya kadar azaltabilirsiniz. Şunu hayal edin:

<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şeni ile ilgilenir, ancak devencies.js'de yardımcı işlevler gerektirir. İdeal olarak tüm komut dosyalarını eşzamansız olarak indirmek, ardından geliştirme komut dosyalarını mümkün olan en kısa sürede, herhangi bir sırada, ancak yine de bağımlılık.js'den sonra çalıştırmak isteriz. Kademeli, progresif bir geliştirme yöntemidir. Ne yazık ki, komut dosyalarının kendileri bağımlılar.js'nin yükleme durumunu izlemek üzere değiştirilmediği sürece, bunu yapmanın bildirim temelli bir yolu yoktur. geliştirme-10.js'nin yürütülmesi 1-9 aralığında engelleyeceğinden async=false bile bu sorunu çözmez. Aslında, bunu hile olmadan mümkün kılan tek bir tarayıcı vardır...

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 şimdi “whatever.js” dosyasını indirmeye başlar; komut dosyası dokümana eklenene kadar diğer tarayıcılar indirme işlemini başlatmaz. IE'de ayrıca, yükleme ilerlemesini bildiren bir "readystatechange" etkinliği ve "readystate" özelliği mevcuttur. Komut dosyalarının yüklenmesini ve yürütülmesini bağımsız olarak kontrol etmemizi sağladığından, bu gerçekten çok faydalı.

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ı ekleyeceğinizi seçerek karmaşık bağımlılık modelleri oluşturabiliriz. IE bu modeli sürüm 6'dan beri desteklemektedir. Oldukça ilginç ancak uygulamada, async=false ile aynı ön yükleyici keşfedilebilirliği sorunu devam ediyor.

Bu kadar! Komut dosyalarını nasıl yüklemeliyim?

Tamam, tamam. Komut dosyalarını oluşturmayı engellemeyen, tekrar içermeyen ve mükemmel tarayıcı desteğine sahip bir şekilde yüklemek istiyorsanız size önerim şudur:

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

İşte. Gövde öğesinin sonunda. Evet, web geliştiricisi olmak Kral Sisyphus olmaya benzer. Yunan mitolojisine atıfta bulunmak için 100 hipster puanı). HTML ve tarayıcılara ilişkin sınırlamalar, daha iyi hizmet sunmamızı engelliyor.

JavaScript modüllerinin, komut dosyalarını yüklemek ve yürütme sırası üzerinde kontrol sağlamak için bildirim temelli engellemeyen bir yöntem sağlayarak bizi kurtaracağını umuyorum. Ancak bu yöntem, komut dosyalarının modül olarak yazılmasını gerektirir.

Eyva, artık kullanabileceğimiz daha iyi bir şey var mı?

Yeterince adil, bonus puanlar için, performans konusunda gerçekten agresif olmak istiyorsanız ve fazla karmaşıklık ve tekrara önem vermiyorsanız yukarıdaki püf noktalarının birkaçını birleştirebilirsiniz.

İlk olarak, ön yükleyiciler için alt kaynak bildirimini ekleriz:

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

Ardından, dokümanın başlığında satır içi olarak komut dosyalarımızı async=false kullanarak JavaScript ile yüklüyoruz ve IE'nin hazır durum tabanlı komut dosyası yüklemesine geri dönüp erteleme işlemini gerçekleştiriyoruz.

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 sadeleştirme sonrasında 362 bayt ve komut dosyası URL'lerinizin boyutu olacak:

!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ına kıyasla ekstra baytlara değer mi? Komut dosyalarını koşullu olarak yüklemek için zaten BBC'nin yaptığı gibi JavaScript kullanıyorsanız bu indirmeleri daha erken tetiklemek de sizin için avantajlı olabilir. Aksi takdirde, basit "vücut sonu" yöntemine bağlı kalınması gerekebilir.

Eyvah, artık WHATWG komut dosyası yükleme bölümünün neden bu kadar geniş olduğunu biliyorum. 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>

Spesifikasyonlar:Birlikte indirin, beklemedeki tüm CSS'lerden sonra sırayla yürütün, işlem tamamlanana kadar oluşturmayı engelleyin. Tarayıcılar şöyle diyor: Evet bay!

Ertele

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

Spesifikasyonlar:Birlikte indirin, DOMContentLoaded öncesinde sırayla yürütülür. "src" içermeyen komut dosyalarında "ertele" seçeneğini yoksayın. IE < 10 diyor ki: 1.js'nin yürütülmesinin ortasında 2.js'yi yürütebilirim. Eğlenceli değil mi? Kırmızı tarayıcılar şu şekilde: Bu "erteleme" işinin ne olduğunu bilmiyorum, komut dosyalarını orada yokmuş gibi yükleyeceğim. Diğer tarayıcılar şöyle diyor: Peki ama "src" olmayan komut dosyalarında "ertele" seçeneğini yoksayamam.

Asenk.

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

Spesifikasyonlar:Birlikte indirin, dilediğiniz sırada yürütün. Kırmızı renkli tarayıcılar şöyle diyor: "Eş zamansız" ne demek? Komut dosyalarını orada yokmuş gibi yükleyeceğim. Diğer tarayıcılar şöyle diyor: Evet, tamam.

Async false

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

Spesifikasyonlar:Birlikte indirin, tüm indirme tamamlandığında sırayla yürütülür. Firefox < 3.6, Opera diyor: Bu "eş zamansız" şeyin ne olduğu konusunda hiçbir fikrim yok ama JS aracılığıyla eklenen komut dosyalarını eklendikleri sırayla çalıştırıyorum. Safari 5.0 diyor ki: "Eş zamansız" durumunu anlıyorum, ancak JS'de "yanlış" olarak ayarlanmasını anlamıyorum. Komut dosyalarınızı gelir gelmez hangi sırayla yürüteceğim. IE < 10 şöyle diyor: "async" hakkında bir fikrim yok, ancak "onreadystatechange" işlevini kullanan bir çözüm vardır. Kırmızı kırmızı renkli diğer tarayıcılarşu anda komut dosyalarınız ne zaman çalışırsa şu şekilde anlar: Diğer her şey şunu ifade eder: Ben senin arkadaşım ve kitap tarafından yapalım.