عندما يُحمّل المتصفّح ملف HTML ويجد وسم <script>...</script>
، فلا يمكنه مواصلة بناء DOM. يجب أن ينفّذ السكربت حالا. نفس الشيء بالنسبة للسكربتات الخارجيّة <script src="...">..</script>
: يجب أن ينتظر المتصفّح تحميل السكربت، ثم ينفّذ السكربت المحمّل، وعندها فقط يمكنه معالجة بقيّة الصفحة.
يرافق ذلك مشكلتان مهمّتان:
- لا يمكن أن تطّلع السكربتات على عناصر DOM التي تحتها، ولا يمكنها إذًا إضافة معالجات وما إلى ذلك.
- إذا كان هناك سكربت كبير في أعلى الصفحة، فإنّه "يحبس الصفحة". لا يمكن للمستخدمين رؤية محتوى الصفحة حتى يُحمّل السكربت ويُنفّذ:
<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>
- يظهر محتوى الصفحة مباشرة.
-
ينتظر معالج الحدث
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
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.