التعمق في المياه الضبابية أثناء تحميل النصوص

مقدمة

سأعلمك في هذه المقالة كيفية تحميل بعض نصوص JavaScript في المتصفح وتنفيذها.

لا، انتظر، عد! أعلم أن الأمر يبدو عاديًا وبسيطًا، ولكن تذكر أن هذا يحدث في المتصفح، حيث تصبح البساطة نظريًا عبارة عن ثقب غير مألوف. تتيح لك معرفة هذه الميزات اختيار الطريقة الأسرع والأقل إزعاجًا لتحميل النصوص البرمجية. إذا كنت تستخدم جدولاً زمنيًا ضيقًا، يمكنك التخطّي إلى المرجع السريع.

في البداية، في ما يلي كيفية تحديد المواصفات للطرق المختلفة التي يمكن أن يتم بها تنزيل النص البرمجي وتنفيذه:

عبارة whatWG عند تحميل النص البرمجي
ماذا يحدث عند تحميل النص البرمجي

مثل كل مواصفات WhatWG، يبدو في البداية وكأنه تداعيات قنبلة عنقودية في مصنع خربشة، ولكن بعد أن تقرأها للمرة الخامسة وتمسح الدم من عينيك، الأمر مثير للاهتمام حقًا:

يتضمن النص الأول لديّ

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

حسنًا، بساطة سعادة. سينزّل المتصفّح هنا كلا النصَين البرمجيَين بالتوازي وينفّذهما في أقرب وقت ممكن، مع الحفاظ على ترتيبهما. لن يتم تنفيذ "2.js" حتى يتم تنفيذ "1.js" (أو فشل في ذلك)، ولن يتم تنفيذ "1.js" إلى أن يتم تنفيذ النص البرمجي أو ورقة الأنماط السابقة، وما إلى ذلك.

للأسف، يحظر المتصفح عرض المزيد من الصفحات أثناء حدوث ذلك. ويرجع ذلك إلى واجهات برمجة تطبيقات DOM من "عصر الويب الأول" التي تسمح بإلحاق السلاسل بالمحتوى الذي يمضغه المحلل اللغوي، مثل document.write. وستواصل المتصفّحات الجديدة فحص المستند أو تحليله في الخلفية وبدء عمليات التنزيل للمحتوى الخارجي الذي قد يحتاجه (مثل js أو الصور أو css أو غير ذلك)، ولكن لا يزال العرض محظورًا.

وهذا هو السبب في أن الجميل في عالم الأداء يوصي بوضع عناصر نصية في نهاية المستند، لأنه يحظر أقل قدر ممكن من المحتوى. وهذا يعني أن المتصفح لا يظهر له النص البرمجي إلى أن يتم تنزيل محتوى HTML بالكامل، وعندئذٍ سيبدأ في تنزيل محتوى آخر، مثل CSS والصور وإطارات iframe. تتمتع المتصفحات الحديثة بذكاء كافٍ لإعطاء الأولوية لـ JavaScript على الصور، غير أنه يمكننا أن نقوم بعمل أفضل.

شكرًا IE! (لا، لا أقصد السخرية)

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

تعرفت شركة Microsoft على مشكلات الأداء هذه وقدمت ميزة "التأجيل" في الإصدار 4 من Internet Explorer. وفقًا لما يلي: "أعِدك بعدم إدخال محتوى في المحلّل باستخدام أدوات مثل document.write. إذا لم أضمن الوعد بذلك، سيكون لك مطلق الحرية في معاقبةي بأي طريقة تراها مناسبة". تحويلت هذه السمة إلى HTML4 وظهرت في متصفّحات أخرى.

في المثال أعلاه، سينزّل المتصفّح كلا النصَّين البرمجيَين بالتوازي وينفّذهما قبل تنشيط DOMContentLoaded مباشرةً، مع الحفاظ على الترتيب.

مثل القنبلة العنقودية في مصنع أغنام، أصبح "تأجيل" فوضى مليئة بالخيال. تتوفّر 6 أنماط لإضافة نص برمجي بين السمتَين "src" و"تأجيل" وعلامات النص البرمجي في مقابل النصوص البرمجية المضافة ديناميكيًا. بطبيعة الحال، لم توافق المتصفحات على الترتيب المطلوب تنفيذها. كتبت Mozilla مقالة رائعة عن هذه المشكلة وهي تظهر في عام 2009.

لقد أوضحت whatWG هذا السلوك بوضوح، حيث أعلنت أنّ "تأجيل" لن يكون له أي تأثير على النصوص البرمجية التي تمت إضافتها ديناميكيًا أو التي تفتقر إلى "src". وبخلاف ذلك، يجب تشغيل النصوص البرمجية المؤجلة بعد تحليل المستند، بترتيب إضافتها.

شكرًا IE! (حسنًا، أشعر بالسخرية الآن)

إنه يعطي، ويأخذه. للأسف، هناك خطأ سيئ في الإصدارات IE4-9 يمكن أن يتسبب في تنفيذ النصوص البرمجية بترتيب غير متوقع. إليك ما يحدث:

1.js

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

2.js

console.log('3');

بافتراض وجود فقرة في الصفحة، فإن الترتيب المتوقع للسجلات هو [1، 2، 3]، على الرغم من أن في IE9 والإصدارات الأقدم تحصل على [1، 3، 2]. تتسبب عمليات DOM الخاصة في إيقاف IE مؤقتًا لتنفيذ النص البرمجي الحالي وتنفيذ نصوص برمجية أخرى معلّقة قبل المتابعة.

ومع ذلك، حتى في عمليات التنفيذ التي لا تتضمّن أخطاء، مثل IE10 والمتصفّحات الأخرى، يتأخر تنفيذ النص البرمجي إلى أن يتم تنزيل المستند بالكامل وتحليله. قد يكون هذا الأمر مريحًا إذا كنت تنتظر DOMContentLoaded على أي حال، ولكن إذا كنت تريد تحسين أداء إعلاناتك، يمكنك البدء بإضافة مستمعين وإطلاق ميزة بدء التشغيل في وقت أقرب...

HTML5 لإنقاذ الهدف

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

قدّم لنا HTML5 سمة جديدة، وهي "غير متزامن"، وتفترض أنك لن تستخدم document.write، إلا أنّك لا تنتظر حتى يتم تحليل المستند لتنفيذه. سينزّل المتصفح كلا النصين البرمجيَين بالتوازي وينفّذهما في أقرب وقت ممكن.

للأسف، بما أنه سيتم تنفيذ "2.js" في أقرب وقت ممكن، قد يتم تنفيذ "2.js" قبل "1.js". وهذا لا بأس به إذا كانت مستقلة، فربما يكون "1.js" نص برمجي للتتبع لا علاقة له بـ "2.js". ولكن إذا كان "1.js" نسخة من CDN من jQuery يعتمد عليها "2.js"، فلن تحصل صفحتك على شيء من الأخطاء ...

نعرف ما نحتاج إليه، مكتبة JavaScript!

يتم تنزيل مجموعة من النصوص البرمجية فورًا بدون حظر العرض وتنفيذها في أقرب وقت ممكن بالترتيب الذي تمت إضافتها به. للأسف، يكرهك HTML ولن يسمح لك بذلك.

عالج JavaScript هذه المشكلة ببعض النكهات. وقد تطلّب منك البعض إجراء تغييرات على لغة JavaScript، مع وضعها في معاودة الاتصال التي تستدعيها المكتبة بالترتيب الصحيح (مثل RequireJS). بينما يستخدم البعض الآخر XHR للتنزيل بالتوازي ثم eval() بالترتيب الصحيح، وهو الأمر الذي لا يصلح للنصوص البرمجية في نطاق آخر ما لم يكن لديهم رأس CORS وكان المتصفح متوافقًا معه. حتى أن بعض هؤلاء استخدموا حيلاً سحرية للغاية، مثل LabJS.

تضمّنت عمليات الاختراق خداع المتصفح لتنزيل المورد بطريقة تؤدي إلى تشغيل حدث عند اكتماله، ولكن مع تجنُّب تنفيذه. في LabJS، ستتم إضافة النص البرمجي باستخدام نوع MIME غير صحيح، مثل <script type="script/cache" src="...">. بعد تنزيل جميع النصوص البرمجية، ستتم إضافتها مرة أخرى بنوع صحيح، على أمل أن يحصل عليها المتصفح مباشرةً من ذاكرة التخزين المؤقت وينفذها على الفور، بالترتيب. واعتمد ذلك على السلوك المريح ولكن غير المحدَّد، وتم تعطُّله عندما كان يجب على المتصفحات التي تم الإعلان عنها في HTML5 عدم تنزيل نصوص برمجية من نوع غير معروف. تجدر الإشارة إلى أنّ LabJS قد تكيف مع هذه التغييرات ويستخدم الآن مجموعة من الطرق الواردة في هذه المقالة.

ومع ذلك، تعاني برامج تحميل النصوص البرمجية من مشكلة في الأداء، ويجب الانتظار إلى أن يتم تنزيل وتحليل لغة JavaScript للمكتبة قبل أن يبدأ تنزيل أي نصوص برمجية تديرها. أيضًا، كيف سنقوم بتحميل برنامج تحميل البرنامج النصي؟ كيف سنحمّل البرنامج النصي الذي يخبر برنامج تحميل النص البرمجي بما يجب تحميله؟ من يراقب صانع المحتوى؟ لماذا أنا عارٍ؟ هذه كلها أسئلة صعبة.

في الأساس، إذا اضطررت إلى تنزيل ملف نص برمجي إضافي قبل التفكير في تنزيل نصوص برمجية أخرى، هذا يعني أنّك ستكون قد خسرت منافسة الأداء مباشرةً.

نموذج كائن المستند (DOM) للإنقاذ

تكمن الإجابة في مواصفات HTML5، على الرغم من أنها مخفية في الجزء السفلي من قسم تحميل النص البرمجي.

لنترجم ذلك إلى كلمة "الأرض":

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

تكون النصوص البرمجية التي يتم إنشاؤها ديناميكيًا وإضافتها إلى المستند غير متزامنة تلقائيًا، وهي لا تحظر العرض وتنفيذه فور تنزيله، ما يعني أنها قد تظهر بترتيب غير صحيح. ومع ذلك، يمكننا بوضوح وضع علامة عليها باعتبارها غير متزامنة:

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

وهذا يعطي نصوصنا النصية مزيجًا من السلوكيات التي لا يمكن تحقيقها باستخدام HTML العادي. نظرًا لعدم مزامنة النصوص البرمجية بشكل صريح، تتم إضافة النصوص البرمجية إلى قائمة انتظار التنفيذ، وهي نفس قائمة الانتظار التي تتم إضافتها إليها في مثال HTML العادي الأول. ومع ذلك، من خلال إنشائها ديناميكيًا، يتم تنفيذها خارج تحليل المستند، لذلك لا يتم حظر العرض أثناء تنزيلها (لا تخلط بين تحميل النص البرمجي غير المتزامن وبين تحميل XHR الذي تتم مزامنته، وهو أمر ليس أمرًا جيدًا على الإطلاق).

يجب تضمين النص البرمجي الوارد أعلاه مضمّنًا في رأس الصفحات، ووضع عمليات تنزيل النصوص البرمجية في قائمة الانتظار في أقرب وقت ممكن بدون تعطيل العرض التدريجي، كما يتم تنفيذه في أقرب وقت ممكن بالترتيب الذي حدّدته. يمكن تنزيل "2.js" مجانًا قبل "1.js"، ولكن لن يتم تنفيذه حتى يتم تنزيل "1.js" وتنفيذه بنجاح، أو تعذُّر تنفيذ أي منهما. عظيم! تنزيل غير متزامن ولكن تم طلب التنفيذ!

يمكن تحميل النصوص البرمجية بهذه الطريقة من خلال كل العناصر التي تتيح السمة غير المتزامنة، باستثناء الإصدار 5.0 من Safari (لا بأس في استخدام الإصدار 5.1). بالإضافة إلى ذلك، يتم دعم جميع إصدارات Firefox وOpera كإصدارات لا تدعم السمة غير المتزامنة، تعمل بسهولة على تنفيذ النصوص البرمجية المضافة ديناميكيًا بترتيب إضافتها إلى المستند على أي حال.

هذه هي أسرع طريقة لتحميل النصوص البرمجية بشكل صحيح؟ أليس كذلك؟

حسنًا، إذا كنت تحدد بشكل ديناميكي النصوص البرمجية المراد تحميلها، فقد يكون نعم أو خلاف ذلك. في المثال أعلاه، على المتصفّح تحليل النص البرمجي وتنفيذه لاكتشاف النصوص البرمجية المطلوب تنزيلها. يؤدي هذا الإجراء إلى إخفاء النصوص البرمجية عن الماسحات الضوئية التي يتم تحميلها مُسبقًا. وتستخدم المتصفّحات هذه أدوات الفحص لاكتشاف الموارد على الصفحات التي من المحتمل أن تزورها بعد ذلك، أو اكتشاف موارد الصفحة عندما يتم حظر المحلل اللغوي بواسطة مورد آخر.

يمكننا إضافة قابلية الاكتشاف مرة أخرى من خلال وضع ذلك في رأس المستند:

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

وهذا يخبر المتصفح بأن الصفحة تحتاج إلى 1.js و2.js. إنّ السمة link[rel=subresource] تشبه link[rel=prefetch]، ولكن مع دلالات مختلفة. وللأسف، لا يتم دعم هذه الميزة حاليًا إلا في Chrome، وعليك تحديد النصوص البرمجية التي تريد تحميلها مرتين، مرة عبر عناصر الروابط ومرة أخرى في النص البرمجي.

تصحيح: لقد ذكرتُ في البداية أنّه تم التقاطها بواسطة ماسح ضوئي للتحميل المسبق، وأنّ المحلّل اللغوي العادي لم يتم التقاطها. ومع ذلك، يمكن لأداة فحص التحميل المسبق استلام هذه الرموز، ولكنها لا تستوفيها بعد، في حين لا يمكن أبدًا تحميل النصوص البرمجية المضمّنة في رمز برمجي تنفيذي مُسبَقًا. شكرًا يواف وايس الذي صحّح لي في التعليقات.

أجد هذه المقالة محبطة

الوضع محبط ويجب أن تشعر بالاكتئاب. ما مِن طريقة تعريفية غير متكررة لتنزيل النصوص البرمجية بشكل سريع وغير متزامن أثناء التحكّم في ترتيب التنفيذ. باستخدام HTTP2/SPDY، يمكنك تقليل أعباء الطلب إلى الحد الذي يمكن أن يكون فيه تسليم النصوص البرمجية في عدة ملفات صغيرة قابلة للتخزين المؤقت بشكل فردي أسرع طريقة. تخيل:

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

يتعامل كل نص برمجي للتحسين مع مكون صفحة معين، غير أنه يتطلب وظائف الأداة في loyaltyiness.js. من الناحية المثالية، نريد تنزيل كل العناصر بشكل غير متزامن، ثم تنفيذ النصوص البرمجية للتحسين في أقرب وقت ممكن، وبأي ترتيب، ولكن بعد Attribution.js. إنه تحسين تدريجي! وليس هناك طريقة بيانية لتحقيق ذلك ما لم يتم تعديل النصوص البرمجية نفسها لتتبع حالة تحميل loyaltyiness.js. حتى async=false لا يحل هذه المشكلة، حيث سيتم حظر تنفيذ Optimize-10.js في 1-9. في الواقع، هناك متصفح واحد فقط يمكنه تحقيق ذلك بدون الاختراق...

لدى IE فكرة!

يحمّل IE النصوص البرمجية بشكل مختلف عن المتصفحات الأخرى.

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

يبدأ IE في تنزيل "whatever.js" الآن، ولا تبدأ المتصفحات الأخرى في التنزيل حتى تتم إضافة النص البرمجي إلى المستند. يحتوي IE أيضًا على حدث "readystatechange" وخاصية "readystate" (جاهز للحالة)، والتي تخبرنا بتقدم التحميل. وهذا في الواقع مفيد حقًا، لأنه يتيح لنا التحكم في تحميل وتنفيذ النصوص البرمجية بشكل مستقل.

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

يمكننا إنشاء نماذج تبعية معقدة عن طريق اختيار وقت إضافة النصوص البرمجية إلى المستند. يدعم IE هذا النموذج منذ الإصدار 6. الأمر مثير للاهتمام، ولكن لا تزال تواجهه المشكلة نفسها التي تواجه async=false في قابلية اكتشاف أداة التحميل المسبق.

تكفي. كيف يمكنني تحميل النصوص البرمجية؟

حسنًا. إذا أردت تحميل نصوص برمجية بطريقة لا تمنع العرض ولا تتضمن التكرار ولديها دعم ممتاز للمتصفح، إليك ما أقترحه:

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

هذا صحيح. في نهاية عنصر النص الأساسي. نعم، كونك مطوّر برامج على الويب يشبه إلى حد كبير كونك صاحب قناة King Sisyphus. 100 نقطة عصرية للإشارة إلى الأساطير اليونانية). تمنعنا القيود في HTML والمتصفحات من تحسين أدائنا.

آمل أن تنقذنا وحدات JavaScript من خلال توفير طريقة تعريفية لا تمنع الحظر لتحميل النصوص البرمجية ومنح التحكم في ترتيب التنفيذ، على الرغم من أنّ هذا يتطلّب كتابة النصوص البرمجية كوحدات.

لا بد من وجود شيء أفضل يمكننا استخدامه الآن؟

بما فيه الكفاية، بالنسبة إلى النقاط الإضافية، إذا أردت تعزيز أدائك بشكل كبير وعدم القلق بشأن القليل من التعقيد والتكرار، يمكنك الجمع بين بعض النصائح أعلاه.

أولاً، نضيف تعريف المورد الفرعي، للحزم المسبق:

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

بعد ذلك، يتم تضمين النصوص البرمجية في رأس المستند باستخدام JavaScript، وذلك باستخدام async=false، ثم ننتقل مرة أخرى إلى تحميل النص البرمجي الجاهز في IE، ثم نعود إلى التأجيل.

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

إليك بعض الحيل ودمجها لاحقًا، سيكون حجمها 362 بايت + عناوين URL للنص البرمجي:

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

هل يستحق الأمر وحدات البايت الإضافية مقارنة بنص برمجي بسيط؟ إذا كنت تستخدم JavaScript حاليًا لتحميل النصوص البرمجية بشكل مشروط، كما تفعل قناة BBC، يمكنك أيضًا الاستفادة من تشغيل هذه التنزيلات في وقت أبكر من السابق. أو ربما لا، التزم بالطريقة البسيطة لنهاية الجسم.

يا للهول، الآن عرفت سبب كبر حجم تحميل النص البرمجي whatWG. أحتاج إلى مشروب.

مرجع سريع

عناصر نص عادي

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

المواصفات: تنزيل البيانات معًا وتنفيذها بالترتيب بعد أي CSS في انتظار المراجعة، وحظر العرض إلى أن تكتمل العملية. تقول المتصفحات: نعم يا سيدي.

تأجيل

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

المواصفات: التنزيل معًا والتنفيذ بالترتيب قبل DOMContentLoaded مباشرةً. تجاهُل "تأجيل" في النصوص البرمجية التي لا تتضمّن "src". يعرض IE < 10 ما يلي: قد أنفذ 2.js في منتصف عملية تنفيذ 1.js. أليس ذلك ممتعًا؟ تظهر المتصفحات التي تظهر باللون الأحمر ما يلي: ليس لدي أي فكرة عن معنى "التأجيل" هذا، سأقوم بتحميل النصوص البرمجية كما لو لم تكن موجودة. تقول المتصفّحات الأخرى: حسنًا، ولكنني قد لا أتجاهل "تأجيل" في النصوص البرمجية التي لا تتضمّن "src".

غير متزامنة

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

المواصفات: يمكنك تنزيل الملفات معًا وتنفيذها بأي ترتيب. تظهر من المتصفحات التي تظهر باللون الأحمر ما يلي: ما الذي يحدث "غير متزامن"؟ سأحمّل النصوص البرمجية كما لو لم تكن موجودة. تقول المتصفحات الأخرى: نعم، حسنًا.

Async false

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

المواصفات: يتم تنزيل البيانات معًا وتنفيذها في أقرب وقت ممكن. في Firefox < 3.6، يقول Opera: إنّه ليس لديّ فكرة عن معنى هذه الميزة "غير المتزامن"، لكن في حال حدوث ذلك، يتم تنفيذ نصوص برمجية تمّت إضافتها من خلال JavaScript بالترتيب الذي تتمّ إضافتها. يعرض Safari 5.0 ما يلي: أفهم كلمة "غير متزامن"، ولكني لا أفهم كيفية ضبطه على "خطأ" في JavaScript. سأنفذ النصوص البرمجية بمجرد وصولها، وبأي ترتيب. مراجع IE < 10: ما مِن فكرة عن "غير متزامن"، ولكن هناك حل بديل عن طريق استخدام "onreadystatechange". تشير المتصفحات الأخرى التي تظهر باللون الأحمر إلى: "لا أفهم هذا الأمر غير المتزامن، وسأتولى تنفيذ النصوص البرمجية فور وصولها، وبأي ترتيب. الرسائل الأخرى: أنا صديقك، وسأتولى إنجاز هذا الأمر خلال الكتاب.