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

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

تتلخّص النصائح الشائعة للحفاظ على سرعة تطبيقات 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
  • الحافة: 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.