تحسين المهام الطويلة

لقد تمّ إبلاغك بأنّه "يجب عدم حظر سلسلة التعليمات الرئيسية" و"تقسيم المهام الطويلة"، ولكن ما المقصود بتنفيذ هذه الإجراءات؟

تتلخّص النصائح الشائعة للحفاظ على سرعة تطبيقات JavaScript في النصائح التالية:

  • "لا تحظر سلسلة المحادثات الرئيسية."
  • "تقسيم المهام الطويلة"

هذه نصيحة رائعة، ولكن ما هو العمل الذي ينطوي عليه ذلك؟ من الجيد استخدام قدر أقل من JavaScript، ولكن هل يعني ذلك تلقائيًا أنّ واجهة المستخدم ستكون أكثر استجابةً؟ ربما، ولكن ربما لا.

لفهم كيفية تحسين المهام في JavaScript، عليك أولاً معرفة المهام وكيفية تعامل المتصفّح معها.

ما هي المهمة؟

المهمة هي أي عمل منفصل يؤديه المتصفّح. ويشمل هذا العمل عرض HTML وCSS وتحليلهما وتشغيل JavaScript وأنواع أخرى من العمل التي قد لا يكون لديك سيطرة مباشرة عليها. من بين كل هذه المهام، يُعدّ JavaScript الذي تكتبه هو المصدر الأكبر للمهام.

تمثيل مرئي لمهمة كما هو موضّح في أداة تحليل الأداء في "أدوات مطوّري البرامج في Chrome" تكون المهمة في أعلى حزمة، مع معالِج حدث النقرة واستدعاء دالة ومزيد من العناصر تحتها. تتضمّن المهمة أيضًا بعض أعمال التقديم على الجانب الأيمن.
مهمة بدأها معالِج أحداث click في، معروضة في أداة تحليل الأداء في "أدوات مطوّري البرامج في Chrome".

تؤثر المهام المرتبطة بلغة JavaScript في الأداء بطريقتَين:

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

تحدث كل هذه الإجراءات في سلسلة المهام الرئيسية، باستثناء عمال الويب وواجهات برمجة التطبيقات المشابهة.

ما هو الموضوع الرئيسي؟

الخيط الرئيسي هو المكان الذي يتم فيه تنفيذ معظم المهام في المتصفّح، وحيث يتم تنفيذ كل مقتطفات JavaScript التي تكتبها تقريبًا.

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

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

مهمة طويلة في أداة تحليل الأداء في "أدوات مطوّري البرامج في Chrome" يتم تمثيل الجزء الذي يتسبب في حدوث تأخير في المهمة (أكثر من 50 ملي ثانية) بنمط من الشرائط المائلة الحمراء.
مهمة طويلة كما هو موضّح في أداة تحليل الأداء في Chrome يتم الإشارة إلى المهام الطويلة بمثلث أحمر في زاوية المهمة، مع ملء الجزء الذي يعرقل المهمة بنمط من الخطوط الحمراء المائلة.

لمنع حظر سلسلة التعليمات الرئيسية لفترة طويلة جدًا، يمكنك تقسيم مهمة طويلة إلى عدة مهام أصغر.

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

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

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

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

الآن بعد أن عرفت مدى أهمية تقسيم المهام، يمكنك التعرّف على كيفية إجراء ذلك في JavaScript.

استراتيجيات إدارة المهام

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

function saveSettings () {
  validateForm
();
  showSpinner
();
  saveToDatabase
();
  updateUI
();
  sendAnalytics
();
}

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

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

ومع ذلك، هناك مشكلة محتملة هنا وهي أنّ JavaScript لا تُنفِّذ كلّ دالة من هذه الدوالّ كمهام منفصلة لأنّها يتم تنفيذها ضمن دالة saveSettings(). هذا يعني أنّه سيتم تنفيذ جميع الدوالّ الخمس كمهمة واحدة.

دالة saveSettings كما هو موضّح في أداة تحليل الأداء في Chrome بينما تستدعي الدالة ذات المستوى الأعلى خمس دوال أخرى، يتم تنفيذ جميع المهام في مهمة واحدة طويلة تحظر سلسلة المحادثات الرئيسية.
دالة واحدة saveSettings() تستدعي خمس دوال. يتم تنفيذ العمل كجزء من مهمة واحدة طويلة ومتكاملة.

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

تأجيل تنفيذ الرمز يدويًا

من الطرق التي استخدمها المطوّرون لتقسيم المهام إلى مهام أصغر هي setTimeout(). باستخدام هذه التقنية، يتم تمرير الدالة إلى setTimeout(). يؤدّي ذلك إلى تأجيل تنفيذ طلب إعادة الاتصال إلى مهمة منفصلة، حتى إذا حدّدت مهلة 0.

function saveSettings () {
 
// Do critical work that is user-visible:
  validateForm
();
  showSpinner
();
  updateUI
();

 
// Defer work that isn't user-visible to a separate task:
  setTimeout
(() => {
    saveToDatabase
();
    sendAnalytics
();
 
}, 0);
}

يُعرف ذلك باسم القيمة المعروضة، وهو يعمل بشكل أفضل مع سلسلة من الدوالّ التي يجب تشغيلها بشكل تسلسلي.

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

function processData () {
 
for (const item of largeDataArray) {
   
// Process the individual item here.
 
}
}

يشكّل استخدام setTimeout() هنا مشكلة بسبب ملاءمة المطوّر، وقد تستغرق معالجة الصفيف الكامل للبيانات وقتًا طويلاً جدًا، حتى إذا تم تنفيذ كل تكرار فردي بسرعة. تؤدي كل هذه العوامل إلى أنّ setTimeout() ليست الأداة المناسبة لهذه المهمة، على الأقل عند استخدامها بهذه الطريقة.

استخدِم async/await لإنشاء نقاط العائد

للتأكّد من تنفيذ المهام المهمة الموجّهة للمستخدم قبل المهام ذات الأولوية الأقل، يمكنك الاستسلام للسلسلة الرئيسية من خلال إيقاف قائمة المهام مؤقتًا لمنح المتصفّح فرصًا لتنفيذ مهام أكثر أهمية.

كما شرحنا سابقًا، يمكن استخدام setTimeout للسماح لسلسلة المهام الرئيسية بالتقدّم. ومع ذلك، للتيسير وسهولة القراءة، يمكنك استدعاء setTimeout ضمن Promise وضبط أسلوب resolve كطريقة للردّ.

function yieldToMain () {
 
return new Promise(resolve => {
    setTimeout
(resolve, 0);
 
});
}

تتمثل فائدة دالة yieldToMain() في أنّه يمكنك awaitها في أيّ دالة async. استنادًا إلى المثال السابق، يمكنك إنشاء صفيف من الدوالّ لتشغيلها، والتسليم إلى الخيط الرئيسي بعد تنفيذ كلّ منها:

async function saveSettings () {
 
// Create an array of functions to run:
 
const tasks = [
    validateForm
,
    showSpinner
,
    saveToDatabase
,
    updateUI
,
    sendAnalytics
 
]

 
// Loop over the tasks:
 
while (tasks.length > 0) {
   
// Shift the first task off the tasks array:
   
const task = tasks.shift();

   
// Run the task:
    task
();

   
// Yield to the main thread:
    await yieldToMain
();
 
}
}

والنتيجة هي أنّ المهمة التي كانت متكاملة تم تقسيمها الآن إلى مهام منفصلة.

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

واجهة برمجة تطبيقات مخصّصة لجدولة المهام

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

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

توافق المتصفّح

  • Chrome: 94
  • ‫Edge: 94
  • Firefox: خلف علامة
  • Safari: غير متوافق

المصدر

توفّر واجهة برمجة التطبيقات Scheduler API الدالة postTask() التي تتيح جدولة المهام بدقة أكبر، وهي إحدى الطرق لمساعدة المتصفّح في تحديد أولويات العمل بحيث تُعطى المهام ذات الأولوية المنخفضة الأولوية للسلسلة الرئيسية. يستخدم postTask() الوعود، ويقبل أحد إعدادات priority الثلاثة التالية:

  • 'background' للمهام ذات الأولوية الأدنى
  • 'user-visible' للمهام ذات الأولوية المتوسطة هذا هو الإعداد التلقائي في حال عدم ضبط priority.
  • 'user-blocking' للمهام الحرجة التي يجب تنفيذها بأولوية عالية.

خذ الرمز البرمجي التالي كمثال، حيث يتم استخدام واجهة برمجة التطبيقات postTask() لتشغيل ثلاث مهام بأعلى أولوية ممكنة، والمهمتَين المتبقيتَين بأدنى أولوية ممكنة.

function saveSettings () {
 
// Validate the form at high priority
  scheduler
.postTask(validateForm, {priority: 'user-blocking'});

 
// Show the spinner at high priority:
  scheduler
.postTask(showSpinner, {priority: 'user-blocking'});

 
// Update the database in the background:
  scheduler
.postTask(saveToDatabase, {priority: 'background'});

 
// Update the user interface at high priority:
  scheduler
.postTask(updateUI, {priority: 'user-blocking'});

 
// Send analytics data in the background:
  scheduler
.postTask(sendAnalytics, {priority: 'background'});
};

في هذه الحالة، يتم جدولة أولوية المهام بطريقة تتيح للمهام التي يمنح المتصفّح لها الأولوية، مثل تفاعلات المستخدمين، أن تُنفَّذ بين المهام الأخرى حسب الحاجة.

دالة saveSettings كما هو موضّح في أداة تحليل الأداء في Chrome، ولكن باستخدام postTask. تقسم دالة postTask كل دالة تنشِئها دالة saveSettings، وتمنح الأولوية لها بحيث تتوفّر فرصة لتنفيذ تفاعل المستخدم بدون حظره.
عند تشغيل saveSettings()، تحدّد الدالة جداول زمنية للدوال الفردية باستخدام postTask(). يتم جدولة الأعمال المهمة الموجَّهة للمستخدمين بأولوية عالية، في حين يتم جدولة الأعمال التي لا يعرف عنها المستخدم ليتم تنفيذها في الخلفية. يتيح ذلك تنفيذ تفاعلات المستخدمين بشكل أسرع، لأنّ العمل مُقسَّم ومُعطى الأولوية بشكلٍ مناسب.

في ما يلي مثال بسيط على كيفية استخدام postTask(). من الممكن إنشاء عناصر TaskController مختلفة يمكنها مشاركة الأولويات بين المهام، بما في ذلك إمكانية تغيير الأولويات لمثيلات TaskController المختلفة حسب الحاجة.

العائد المضمّن مع المتابعة باستخدام واجهة برمجة التطبيقات scheduler.yield()

توافق المتصفّح

  • Chrome: ‏ 129
  • ‫Edge: 129
  • Firefox: غير متوافق
  • Safari: غير متوافق

المصدر

scheduler.yield() هي واجهة برمجة تطبيقات مصمّمة خصيصًا للسماح لسلسلة المحادثات الرئيسية في المتصفّح بالعمل. يشبه استخدامها استخدام الدالة yieldToMain() الموضّحة سابقًا في هذا الدليل:

async function saveSettings () {
 
// Create an array of functions to run:
 
const tasks = [
    validateForm
,
    showSpinner
,
    saveToDatabase
,
    updateUI
,
    sendAnalytics
 
]

 
// Loop over the tasks:
 
while (tasks.length > 0) {
   
// Shift the first task off the tasks array:
   
const task = tasks.shift();

   
// Run the task:
    task
();

   
// Yield to the main thread with the scheduler
   
// API's own yielding mechanism:
    await scheduler
.yield();
 
}
}

هذا الرمز مألوف إلى حد كبير، ولكن بدلاً من استخدام yieldToMain()، يتم استخدام await scheduler.yield().

ثلاثة مخطّطات بيانية تصوّر المهام بدون تسليم، وتسليم، وتسليم ومتابعة. بدون التنازل، هناك مهام طويلة. عند استخدام وضع "الاستسلام"، تكون هناك المزيد من المهام الأقصر، ولكن قد تقاطعها مهام أخرى غير ذات صلة. من خلال التوقّف المؤقت والمتابعة، تتوفّر المزيد من المهام الأقصر، ولكن يتم الحفاظ على ترتيب تنفيذها.
عند استخدام scheduler.yield()، يتابع تنفيذ المهمة من حيث توقف حتى بعد نقطة الاستسلام.

تتمثل فائدة scheduler.yield() في الاستمرار، ما يعني أنّه في حال إيقاف المعالجة في منتصف مجموعة من المهام، ستستمر المهام المُجدوَلة الأخرى بالترتيب نفسه بعد نقطة الإيقاف. يؤدي ذلك إلى منع الرموز البرمجية من النصوص البرمجية التابعة لجهات خارجية من مقاطعة ترتيب تنفيذ الرمز البرمجي.

عدم استخدام isInputPending()

توافق المتصفّح

  • Chrome: 87
  • ‫Edge: 87
  • Firefox: غير متوافق
  • Safari: غير متوافق

المصدر

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

يتيح ذلك لـ JavaScript مواصلة العمل في حال عدم توفّر أي إدخالات معلّقة، بدلاً من التوقّف والانتقال إلى نهاية قائمة المهام. ويمكن أن يؤدّي ذلك إلى تحسينات مُبهرة في الأداء، كما هو موضّح بالتفصيل في Intent to Ship، وذلك للمواقع الإلكترونية التي قد لا تعود إلى سلسلة المحادثات الرئيسية بخلاف ذلك.

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

  • قد يعرض isInputPending() خطأً القيمة false على الرغم من تفاعل المستخدِم في بعض الحالات.
  • الإدخال ليس الحالة الوحيدة التي يجب أن تؤدي فيها المهام إلى نتيجة. يمكن أن تكون الصور المتحركة وغيرها من التعديلات العادية على واجهة المستخدم مهمة بنفس القدر لتوفير صفحة ويب متجاوبة.
  • ومنذ ذلك الحين، تمّت إضافة واجهات برمجة تطبيقات أكثر شمولية تعالج المخاوف المتعلّقة بتحقيق الربح، مثل scheduler.postTask() وscheduler.yield().

الخاتمة

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

  • يجب أن تتخلّى عن السلسلة الرئيسية للمهام المهمة الموجَّهة للمستخدمين.
  • تحديد أولويات المهام باستخدام postTask()
  • ننصحك بتجربة scheduler.yield().
  • أخيرًا، ابذل أقل قدر ممكن من الجهد في دوالّك.

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

نشكر فيليب والتون على الفحص الفني لهذا الدليل.

صورة مصغّرة مصدرها Unsplash، من إبداع Amirali Mirhashemian.