تاريخ النشر: 6 آذار (مارس) 2025
تبدو الصفحة بطيئة ولا تستجيب عندما تشغل المهام الطويلة سلسلة المهام الرئيسية، ما يمنع تنفيذ مهام مهمة أخرى، مثل الاستجابة لبيانات المستخدم. ونتيجةً لذلك، قد تبدو عناصر التحكّم في النماذج المضمّنة متعطّلة للمستخدمين، كما لو كانت الصفحة متجمّدة، ناهيك عن المكونات المخصّصة الأكثر تعقيدًا.
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
جزءًا من Prioritized Task Scheduling API. بصفتنا مطوّري ويب، لا نتحدث عادةً عن الترتيب الذي تنفِّذ به حلقة الأحداث المهام من حيث الأولويات الصريحة، ولكن الأولويات النسبية تكون متوفّرة دائمًا، مثل تشغيل طلب إعادة الاتصال requestIdleCallback
بعد أي طلبات إعادة اتصال setTimeout
في قائمة الانتظار، أو تشغيل مستمع أحداث الإدخال الذي تم تشغيله عادةً قبل مهمة تم وضعها في قائمة الانتظار باستخدام setTimeout(callback, 0)
.
تجعل ميزة "تحديد أولوية المهام في الجدولة" هذا الأمر أكثر وضوحًا، ما يسهّل معرفة المهمة التي سيتم تنفيذها قبل الأخرى، كما تتيح تعديل الأولويات لتغيير ترتيب التنفيذ هذا، إذا لزم الأمر.
كما ذكرنا، يحصل تنفيذ الدالة بشكلٍ متواصل بعد الخروج باستخدام scheduler.yield()
على أولوية أعلى من بدء مهام أخرى. ويتمثل المفهوم الإرشادي في أنّه يجب تنفيذ متابعة المهمة أولاً قبل الانتقال إلى المهام الأخرى. إذا كانت المهمة عبارة عن رمز برمجي يعمل بشكل جيد ويتخلّى عن الموارد بشكل دوري كي يتمكّن المتصفّح من تنفيذ مهام مهمة أخرى (مثل الاستجابة لإدخالات المستخدم)، يجب عدم معاقبة هذه المهمة على التخلي عن الموارد من خلال منح الأولوية للمهام الأخرى المشابهة.
في ما يلي مثال: دالتَان تم إدراجهما في قائمة الانتظار لتشغيلهما في مهام مختلفة باستخدام setTimeout
.
setTimeout(myJob);
setTimeout(someoneElsesJob);
في هذه الحالة، يكون طلبَا setTimeout
بجانب بعضهما مباشرةً، ولكن في صفحة حقيقية، يمكن استدعاؤهما في مواضع مختلفة تمامًا، مثل نص برمجي تابع للجهة الأولى ونص برمجي تابع لجهة خارجية يُعدّان العمل بشكل مستقل لتشغيله، أو يمكن أن تكون مهمتَين من مكوّنات منفصلة يتم تشغيلهما في عمق جدولة إطار العمل.
في ما يلي شكل هذا العمل في "أدوات المطوّر":
تم وضع علامة على 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
، إليك كيفية تنفيذ الإجراء:
لقد قسمتنا المهمة إلى 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();
}
يبدو التنفيذ الآن على النحو التالي:
لا يزال للمتصفّح فرصة الاستجابة، ولكن يتم الآن منح الأولوية لمواصلة مهمة 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
هو عنصر polyfill صغير لـ scheduler.postTask
وscheduler.yield
يستخدم داخليًا مجموعة من الطرق لمحاكاة الكثير من إمكانات واجهات برمجة التطبيقات الخاصة بالجدولة في المتصفّحات الأخرى (على الرغم من أنّ اكتساب الأولوية في scheduler.yield()
غير متاح).
بالنسبة إلى المستخدمين الذين يريدون تجنُّب استخدام polyfill، يمكنهم استخدام setTimeout()
وقبول فقدان المتابعة ذات الأولوية، أو حتى عدم الاستسلام في المتصفّحات غير المتوافقة إذا لم يكن ذلك مقبولًا. اطّلِع على مستندات scheduler.yield()
في "أدوات تحسين الأداء من Google" للاطّلاع على مزيد من المعلومات حول المهام الطويلة.
يمكن أيضًا استخدام أنواع wicg-task-scheduling
للحصول على ميزة التحقّق من النوع ودعم IDE إذا كنت بصدد رصد scheduler.yield()
وإضافة عنصر احتياطي بنفسك.
مزيد من المعلومات
لمزيد من المعلومات حول واجهة برمجة التطبيقات وكيفية تفاعلها مع أولويات المهام وscheduler.postTask()
، يمكنك الاطّلاع على مستندات scheduler.yield()
وجدولة المهام ذات الأولوية على MDN.
لمزيد من المعلومات عن المهام الطويلة وتأثيرها في تجربة المستخدم وكيفية التعامل معها، يمكنك الاطّلاع على مقالة تحسين المهام الطويلة.