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

تتضمن النصيحة الشائعة المتاحة لزيادة سرعة تطبيقات JavaScript في أغلب الأحيان ما يلي: "عدم حظر سلسلة التعليمات الرئيسية" و"تقسيم المهام الطويلة". تقسّم هذه الصفحة ما تعنيه هذه النصيحة، وتشرح أهمية تحسين المهام في JavaScript.

ما المهمة؟

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

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

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

ما هي سلسلة التعليمات الرئيسية؟

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

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

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

لمنع هذا، قم بتقسيم كل مهمة طويلة إلى مهام أصغر تستغرق كل مهمة وقتًا أقل لتشغيلها. ويُسمى ذلك تقسيم المهام الطويلة.

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

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

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

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

تتعامل لغة JavaScript مع كل دالة كمهمة واحدة، لأنّها تستخدم نموذج عملية التنفيذ السريع لتنفيذ المهمة. يعني هذا أن الدالة التي تستدعي دوال أخرى متعددة، مثل المثال التالي، يجب أن تعمل حتى تكتمل جميع الدوال المسماة، مما يؤدي إلى إبطاء المتصفح:

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
دالة SaveSettings المعروضة في محلّل أداء Chrome. بينما تستدعي دالة المستوى الأعلى خمس دوال أخرى، يتم تنفيذ جميع الأعمال في مهمة واحدة طويلة تحظر سلسلة التعليمات الرئيسية.
دالة واحدة saveSettings() تستدعي خمس دوال. يتم تنفيذ العمل كجزء من مهمة واحدة طويلة متجانسة.

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

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

يمكنك تأجيل تنفيذ بعض المهام عن طريق تمرير الدالة ذات الصلة إلى 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);
}

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

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

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

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

استخدام async/await لإنشاء نقاط ربح

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

أوضح طريقة لتنفيذ ذلك تشمل Promise الذي يتم حلّه من خلال طلب الرقم setTimeout():

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

في الدالة saveSettings()، يمكنك الرجوع إلى سلسلة التعليمات الرئيسية بعد كل خطوة في حال await من خلال الدالة 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:
    await yieldToMain();
  }
}

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

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

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

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

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

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

  • 94
  • 94
  • x

المصدر

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

هناك ثلاث أولويات متوفّرة لواجهة برمجة التطبيقات postTask():

  • '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(). حيث يتم تحديد أولوية عالية للعمل الموجَّه للمستخدمين، في حين تتم جدولة العمل الذي لا يعرفه المستخدم في الخلفية. ويسمح هذا الإجراء بتفاعل المستخدمين بسرعة أكبر، لأنّ العمل يتم تقسيمه وتحديد أولوياته بشكلٍ مناسب.

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

أرباح مدمجة مع الاستمرار في استخدام واجهة برمجة التطبيقات scheduler.yield() القادمة

نقطة رئيسية: للحصول على شرح أكثر تفصيلاً عن scheduler.yield()، يمكنك الاطّلاع على مرحلة التجربة والتقييم (منذ أن تم الانتهاء منها)، بالإضافة إلى شرحها.

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

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

حاصل على الإدخال باستخدام isInputPending()

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

  • 87
  • 87
  • x
  • x

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

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

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

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

الخلاصة

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

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

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

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

تم الحصول على صورة مصغّرة من UnLaunch، وهي مقدّمة من أميرالي ميرهاشيميان.