اذهب إلى المحتوى

شرح اللاتزامن async وعدم الانتظار defer أثناء تحميل سكربتات جافاسكربت في صفحات HTML


محمد أمين بوقرة

عندما يُحمّل المتصفّح ملف HTML ويجد وسم <script>...</script>، فلا يمكنه مواصلة بناء DOM. يجب أن ينفّذ السكربت حالا. نفس الشيء بالنسبة للسكربتات الخارجيّة ‎<script src="...">..</script>‎: يجب أن ينتظر المتصفّح تحميل السكربت، ثم ينفّذ السكربت المحمّل، وعندها فقط يمكنه معالجة بقيّة الصفحة.

يرافق ذلك مشكلتان مهمّتان:

  1. لا يمكن أن تطّلع السكربتات على عناصر DOM التي تحتها، ولا يمكنها إذًا إضافة معالجات وما إلى ذلك.
  2. إذا كان هناك سكربت كبير في أعلى الصفحة، فإنّه "يحبس الصفحة". لا يمكن للمستخدمين رؤية محتوى الصفحة حتى يُحمّل السكربت ويُنفّذ:
<p>...content before script...</p>

<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- هذا غير مرئيّ إلى أن يُحمّل السكربت -->
<p>...content after script...</p>

هناك طرق للالتفاف حول تلك المشاكل. على سبيل المثال، يمكننا وضع السكربت في أسفل الصفحة. وبذلك يمكنه الاطلاع على العناصر التي فوقه، ولا تُحبس محتويات الصفحة عن الظهور:

<body>
  ...all content is above the script...

  <script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>

لكنّ هذه الطريقة بعيدة عن كونها مثاليّة. على سبيل المثال، لا يعلم المتصفّحُ بوجود السكربت (ولا يمكنه البدء بتحميله) إلا بعد تحميل كامل مستند HTML. بالنسبة لمستندات HTML الطويلة، قد يكون هناك تأخّر ملحوظ.

لا تُلاحظ هذه الأمور لمن يملكون اتصالات سريعة جدا، لكن لا يزال الكثير من الناس حول العالم يملكون اتصالات بطيئة بالانترنت ويستخدمون اتصالات انترنت محمولة أبعد ما تكون عن المثاليّة.

لحسن الحظّ، هناك سمتان للعنصر <script> يمكنها أن تحلّ لنا هذه المشكلة: defer وasync.

السمة defer

تطلب السمة defer من المتصفّح أن لا ينتظر السكربت. بل يواصل المتصفّح معالجة HTML وبناء DOM. يُحمّل السكربت في "الخلفيّة"، ثمّ يشتغل عندما يتمّ بناء DOM كاملا.

إليك نفس المثال الذي في الأعلى لكن باستخدام defer:

<p>...content before script...</p>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- يكون مرئيّا على الفور -->
<p>...content after script...</p>

بعبارة أخرى:

  • لا تحبس السكربتات التي لها defer الصفحة أبدا.
  • دائما ما تُنفّذ السكربتات التي لها defer عندما يكون DOM جاهزا (لكن قبل الحدث DOMContentLoaded).

يوضّح هذا المثال النقطة الثانية:

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<p>...content after scripts...</p>

  1. يظهر محتوى الصفحة مباشرة.
  2. ينتظر معالج الحدث DOMContentLoaded السكربت المُرجأ (deferred). لا يشتغل حتى يُحمّل السكربت ويُنفّذ.

تحافظ السكربتات المُرجأة على الترتيب بينها، تماما كما هو الحال مع السكربتات العاديّة

لنفترض أنّ لدينا سكربتين مُرجأين: الطويل long.jsوثمّ القصير small.js:

<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>

يبحث المتصفّح في الصفحة على السكربتات ويُحمّلها معًا، لتحسين الأداء. ففي المثال أعلاه يُحمّل السكربتان بشكل متوازي. من المُرجّح أن ينتهي small.js أوّلا.

لكنّ السمة defer، بالإضافة إلى طلبها من المتصفّح "عدم الحبس"، تضمن المحافظة على الترتيب بين السكربتات. فحتى لو حُمّل small.js أوّلا، فإنّ عليه أن ينتظر ويُنفّذ بعد تنفيذ long.js.

قد يكون ذلك مهمّا في الحالات التي نحتاج فيها إلى تحميل مكتبة جافاسكربت أوّلا ثمّ سكربت آخر يعتمد عليها.


ملاحظة: السمة defer هي للسكربتات الخارجيّة فقط

تُتجاهل السمة defer إذا لم يكن الوسم <script> له src.


async

السمة async هي مثل defer نوعا ما. هي أيضا تجعل السكربت غير حابس. لكنّ بينهما فروقات مهمّة في السلوك.

تعني السمة async أن السكربت مستقلّ تماما:

  • لا يحبس المتصفّح عند السكربتات التي لها async (مثل defer).
  • لا تنتظر السكربتات الأخرى السكربتات التي لها async، ولا تنتظر السكربتات التي لها async السكربتات الأخرى.
  • لا ينتظرDOMContentLoaded والسكربتات غير المتزامنة بعضها البعض:
    • قد يقع DOMContentLoaded سواء قبل السكربت غير المتزامن (عندما ينتهي تحميل السكربت غير المتزامن قبل تمام الصفحة)
    • …أو بعد السكربت غير المتزامن (إذا كان السكربت غير المتزامن قصيرا أو كان في ذاكرة التخزين المؤقّت HTTP).

بعبارة أخرى، تُحمّل السكربتات التي لها async في الخلفيّة وتُنفّذ عندما تجهز. لا ينتظرها DOM ولا السكربتات الأخرى، ولا تنتظر هي بدورها أيّ شيء. سكربت مستقلّ تماما يُنفّذ عندما يجهز. من أبسط ما يكون، صحيح؟

إليك مثالا مشابها للذي رأينا مع defer: سكربتان long.js وsmall.js، لكن الآن لها async بدل defer.

لا ينتظر أحدهما الآخر. أيّا كان الذي يُحمّل أوّلا (small.js على الأرجح) فإنّه يُنفّذ أوّلا:

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>

<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>

<p>...content after scripts...</p>

  • يظهر محتوى الصفحة فورا: لا يحبسه async.
  • قد يقع DOMContentLoaded إمّا قبل async أو بعد، لا ضمانات هنا.
  • يأتي السكربت الأقصر small.js ثانيا، لكنّه على الأرجح يُحمّل قبل long.js، وبالتالي فإنّ small.js يُنفّذ أوّلا. رغم ذلك، من الممكن أن يُحمّل long.js أوّلا، إذا كان في ذاكرة التخزين المؤقّتة، فينُفّذ بذلك أوّلا. بعبارة أخرى، تُنفّذ السكربتات غير المتزامنة حسب ترتيب "أوّليّة التحميل".

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

<!-- هكذا عادة Google Analytics تضاف-->
<script async src="https://google-analytics.com/analytics.js"></script>

السكربتات الديناميكيّة

هناك طريقة مهمّة أخرى لإضافة سكربت إلى الصفحة.

يمكننا إنشاء سكربت وإلحاقه بالمستند ديناميكيّا باستخدام جافاسكربت:

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

يبدأ تحميل السكربت بعد إلحاقه بالمستند مباشرة (*).

تتصرّف السكربتات الديناميكيّة افتراضيًّا وكأنّها "غير متزامنة"، يعني ذلك:

  • لا تنتظر أيّ شيء، ولاشيء ينتظرها.
  • السكربت الذي يُحمّل أوّلا يُنفّذ أوّلا (حسب ترتيب "أوّليّة التحميل").

يمكن تغيير ذلك إذا وضعنا script.async=false صراحة. عندها تُنفّذ السكربتات حسب ترتيبها في المستند، تماما مثل defer.

في هذا المثال، تضيف الدالّة loadScript(src)‎ سكربتا وتضبط أيضا async على false.

وبذلك يُنفّذ long.js أوّلا على الدوام (إذ قد أُضيف أوّلا):

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// async=false أوّلا لأنّ long.js يُنفّذ
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");

بدون script.async=false، تُنفّذ السكربتات حسب الترتيب الافتراضيّ "أوّليّة التحميل" (small.js أوّلا على الأرجح).

ثانيا، كما هو الأمر مع defer، يصير الترتيب مهمّا عندما نريد تحميل مكتبة ما ومن ثمّ سكربت يعتمد عليها.

الملخص

لكلّ من async وdefer أمر مشترك: لا يحبس تحميلُ هذه السكربتات تصييرَ (rendering) الصفحة. وبذلك يمكن للمستخدم الاطلاع على الصفحة وقراءة محتواها فورا.

لكنّ هناك أيضا فروقات جوهريّة بينها:

  الترتيب DOMContentLoaded
async الترتيب وفق أوليّة التحميل. لا يهمّ ترتيبها في المستند -- الذي يُحمّل أوّلا يُنفّذ أوّلا لا علاقة له. قد يُحمّل السكربت ويُنفّذ قبل أن يتمّ تحميل كامل المستند. يحصل هذا إذا كانت السكربتات صغيرة أو مخزّنة في الذاكرة المؤقّتة، والمستند طويل كفاية.
defer الترتيب وفق المستند (حسب موضعها في المستند). تُنفّذ السكربتات بعد الانتهاء من تحميل المستند وتفسيره (تنتظر عند الحاجة لذلك)، مباشرة قبل DOMContentLoaded.

عمليّا، تُستخدم defer في السكربتات التي تحتاج DOM كاملا و/أو التي يكون ترتيب التنفيذ فيما بينها مهمّا.

وتُستخدم async في السكربتات المستقلّة، مثل العدّادات والإعلانات. ويكون ترتيب تنفيذها غير مهمّ.


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

يُرجى التنبه: إذا استخدمت defer أو async، فسيرى المستخدم الصفحة قبل تحميل السكربت.

في هذه الحالة، قد لا تكون بعض المكوّنات الرسوميّة تهيّئت بعد.

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


ترجمة -وبتصرف- للمقال Scripts: async, defer من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor


تفاعل الأعضاء

أفضل التعليقات



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...