استخدام scheduler.yield() لتقسيم المهام الطويلة

Brendan Kenny
Brendan Kenny

تاريخ النشر: 6 مارس 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

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

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

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

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

تتمثّل الميزة المحدّدة لـ scheduler.yield في أنّه من المقرّر أن يتم تنفيذ عملية المتابعة بعد العائد قبل تنفيذ أي مهام أخرى مماثلة تم وضعها في قائمة الانتظار من قِبل الصفحة. ويعطي الأولوية لمواصلة مهمة على البدء في مهام جديدة.

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

استئناف العمليات ذات الأولوية بعد التوقف

scheduler.yield هي جزء من واجهة برمجة التطبيقات لجدولة المهام ذات الأولوية. بصفتنا مطوّري ويب، لا نتحدث عادةً عن ترتيب تنفيذ المهام في حلقة الأحداث من حيث الأولويات الواضحة، ولكن الأولويات النسبية تكون دائمًا موجودة، مثل تنفيذ معاودة الاتصال requestIdleCallback بعد أي معاودات اتصال setTimeout في قائمة الانتظار، أو تنفيذ أداة معالجة أحداث الإدخال التي تم تشغيلها عادةً قبل مهمة تمت إضافتها إلى قائمة الانتظار باستخدام setTimeout(callback, 0).

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

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

إليك مثال: دالتان تم وضعهما في قائمة الانتظار لتنفيذهما في مهام مختلفة باستخدام setTimeout.

setTimeout(myJob);
setTimeout(someoneElsesJob);

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

في ما يلي الشكل الذي قد يبدو عليه هذا العمل في "أدوات مطوّلي البرامج":

مهمّتان معروضتان في لوحة الأداء ضمن "أدوات مطوّري البرامج في Chrome" يُشار إلى أنّ كلتيهما مهمّتان طويلتان، حيث تستغرق الدالة "myJob" عملية التنفيذ الكاملة للمهمة الأولى، وتستغرق الدالة "someoneElsesJob" عملية التنفيذ الكاملة للمهمة الثانية.

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

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

بما أنّه تمت جدولة myJobPart2 للتشغيل مع setTimeout خلال myJob، ولكن يتم تنفيذ هذه الجدولة بعد أن تتم جدولة someoneElsesJob، إليك طريقة التنفيذ:

ثلاث مهام معروضة في لوحة الأداء ضمن "أدوات مطوّري البرامج في Chrome" تنفّذ المهمة الأولى الدالة myJobPart1، وتنفّذ المهمة الثانية الطويلة الدالة someoneElsesJob، وتنفّذ المهمة الثالثة الدالة myJobPart2.

لقد قسّمنا المهمة باستخدام setTimeout لكي يتمكّن المتصفّح من الاستجابة أثناء منتصف myJob، ولكن الآن لا يتم تنفيذ الجزء الثاني من myJob إلا بعد انتهاء someoneElsesJob.

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

أدخِل scheduler.yield()، ما يضع استمرار أي دالة تستدعيها في قائمة انتظار ذات أولوية أعلى قليلاً من بدء أي مهام أخرى مماثلة. في حال تم تغيير myJob لاستخدامه:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

يبدو التنفيذ الآن على النحو التالي:

مهمّتان معروضتان في لوحة الأداء ضمن "أدوات مطوّري البرامج في Chrome" يُشار إلى أنّ كلتيهما مهمّتان طويلتان، حيث تستغرق الدالة "myJob" عملية التنفيذ الكاملة للمهمة الأولى، وتستغرق الدالة "someoneElsesJob" عملية التنفيذ الكاملة للمهمة الثانية.

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

اكتساب الأذونات من الموقع

في إطار واجهة برمجة التطبيقات الأكبر Prioritized Task Scheduling API، تتوافق scheduler.yield() بشكل جيد مع الأولويات الواضحة المتاحة في scheduler.postTask(). بدون تحديد أولوية بشكل صريح، سيتصرف scheduler.yield() ضمن معاودة الاتصال scheduler.postTask() بشكل أساسي مثل المثال السابق.

ومع ذلك، في حال ضبط أولوية، مثل استخدام أولوية 'background' منخفضة:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

سيتم تحديد موعد استئناف المهمة بأولوية أعلى من مهام 'background' الأخرى، ما يتيح لك الحصول على الاستئناف المتوقّع ذي الأولوية قبل أي عمل 'background' معلّق، ولكن بأولوية أقل من المهام الأخرى التلقائية أو ذات الأولوية العالية، وستبقى المهمة 'background'.

هذا يعني أنّه إذا جدولت عملاً منخفض الأولوية باستخدام 'background' scheduler.postTask() (أو requestIdleCallback)، سيتم أيضًا تأجيل استئناف العمل بعد scheduler.yield() إلى حين اكتمال معظم المهام الأخرى وتوقّف سلسلة التعليمات الرئيسية عن العمل، وهو ما تريده بالضبط من إيقاف العمل مؤقتًا في مهمة منخفضة الأولوية.

كيفية استخدام واجهة برمجة التطبيقات

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

scheduler-polyfill هو برنامج صغير لتعبئة scheduler.postTask وscheduler.yield يستخدم داخليًا مجموعة من الطرق لمحاكاة الكثير من إمكانات واجهات برمجة التطبيقات الخاصة بالجدولة في المتصفحات الأخرى (مع أنّ ميزة scheduler.yield() الخاصة بتوريث الأولوية غير متاحة).

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

يمكن أيضًا استخدام wicg-task-scheduling أنواع للحصول على ميزة التحقّق من الأنواع ودعم بيئة التطوير المتكاملة (IDE) إذا كنت ترصد الميزة scheduler.yield() وتضيف رمزًا احتياطيًا بنفسك.

مزيد من المعلومات

لمزيد من المعلومات حول واجهة برمجة التطبيقات وكيفية تفاعلها مع أولويات المهام وscheduler.postTask()، يمكنك الاطّلاع على مستندات scheduler.yield() وجدولة المهام حسب الأولوية على شبكة مطوّري Mozilla.

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