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

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

عادةً ما تتلخّص النصائح الشائعة للحفاظ على سرعة تطبيقات 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 طريقة فعالة لتقسيم المهام، ولكن قد يكون لها عيب: عندما يتم الرجوع إلى سلسلة التعليمات الرئيسية من خلال تأجيل تشغيل الرمز في مهمة لاحقة، تتم إضافة هذه المهمة إلى نهاية قائمة الانتظار.

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

دعم المتصفح

  • 94
  • 94
  • x

المصدر

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

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

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

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() API القادمة

من الإضافات المقترحة إلى واجهة برمجة التطبيقات لـ الجدولة 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() في الاستمرارية، مما يعني أنه إذا كنت تصل في منتصف مجموعة من المهام، ستستمر المهام المجدولة الأخرى بنفس الترتيب بعد نقطة الإنتاج. يؤدي ذلك إلى تجنّب تعطّل الرمز البرمجي من النصوص البرمجية التابعة لجهات خارجية ترتيب تنفيذ الرمز البرمجي.

هناك أيضًا احتمال كبير بمواصلة استخدام scheduler.postTask() مع priority: 'user-blocking' بسبب أولوية user-blocking العالية، لذا يمكن استخدام هذه الطريقة كبديل في الوقت الحالي.

ويؤدي استخدام setTimeout() (أو scheduler.postTask() مع priority: 'user-visibile' أو بدون priority صريح) إلى جدولة المهمة في نهاية قائمة الانتظار، ما يتيح تنفيذ المهام الأخرى المعلَّقة قبل متابعتها.

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

دعم المتصفح

  • 87
  • 87
  • x
  • x

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

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

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

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

الخلاصة

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

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

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

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

مصدر الصورة المصغّرة: Unسباش، مقدمة من أميرالي ميرهاشيميان.