وصلك إشعار "عدم حظر سلسلة المحادثات الرئيسية". و"تقسيم مهامك الطويلة"، ولكن ماذا يعني القيام بهذه الأشياء؟
عادةً ما تتلخّص النصائح الشائعة للحفاظ على سرعة تطبيقات JavaScript في الحصول على النصائح التالية:
- "لا تحظر سلسلة المحادثات الرئيسية".
- "قسِّم مهامك الطويلة".
هذه نصيحة رائعة، ولكن ما العمل الذي تنطوي عليه؟ إنّ استخدام JavaScript أقل خيار جيد، ولكن هل يعادل ذلك تلقائيًا واجهات مستخدم أكثر استجابة؟ ربما، لكن ربما لا.
لفهم كيفية تحسين المهام في JavaScript، عليك أولاً معرفة المهام وكيفية تعامل المتصفّح معها.
ما هي المهمة؟
المهمة هي أي عمل منفصل يؤديه المتصفّح. ويشمل هذا العمل العرض وتحليل HTML وCSS وتشغيل JavaScript وأنواع أخرى من العمل قد لا يمكنك التحكّم فيها بشكل مباشر. من بين كل هذا، ربما تكون لغة JavaScript التي تكتبها هي أكبر مصدر للمهام.
تؤثر المهام المرتبطة بلغة JavaScript في الأداء بطريقتين:
- عندما ينزِّل متصفّح ملف JavaScript أثناء بدء التشغيل، فإنّه يضع المهام في قائمة انتظار لتحليل لغة JavaScript وتجميعها كي يمكن تنفيذها لاحقًا.
- وفي أوقات أخرى أثناء عمر الصفحة، يتم وضع المهام في قائمة الانتظار عندما تعمل لغة JavaScript، مثل جذب التفاعلات من خلال معالِجات الأحداث والرسوم المتحركة المستندة إلى JavaScript والنشاط في الخلفية مثل مجموعة الإحصاءات.
تحدث كل هذه العناصر في سلسلة التعليمات الرئيسية، باستثناء عاملي الويب وواجهات برمجة التطبيقات المشابهة.
ما هي سلسلة التعليمات الرئيسية؟
سلسلة المحادثات الرئيسية هي المكان الذي يتم فيه تنفيذ معظم المهام في المتصفّح، ويتم تنفيذ جميع رموز JavaScript التي تكتبها تقريبًا.
يمكن لسلسلة المحادثات الرئيسية معالجة مهمة واحدة فقط في كل مرة. أي مهمة تستغرق أكثر من 50 مللي ثانية تكون مهمة طويلة. بالنسبة إلى المهام التي تتجاوز مدتها 50 ملي ثانية، يُعرف الوقت الإجمالي للمهمة مطروحًا منه 50 ملي ثانية باسم فترة حظر المهمة.
يحظر المتصفّح حدوث التفاعلات أثناء تشغيل مهمة بأي طول، إلا أن ذلك لا يلاحظه المستخدم طالما أن المهام لا يتم تشغيلها لفترة طويلة. وعندما يحاول المستخدم التفاعل مع صفحة عندما يكون هناك العديد من المهام الطويلة، لن تستجيب واجهة المستخدم، وقد تتعطل إذا تم حظر سلسلة التعليمات الرئيسية لفترات زمنية طويلة جدًا.
لمنع حظر سلسلة التعليمات الرئيسية لفترة طويلة جدًا، يمكنك تقسيم المهمة الطويلة إلى عدة مهام أصغر.
وهذا مهم، لأنّه عندما يتم تقسيم المهام، يمكن للمتصفّح الاستجابة للعمل ذي الأولوية الأعلى بشكل أسرع بكثير، بما في ذلك تفاعلات المستخدم. بعد ذلك، يتم تشغيل المهام المتبقية حتى الانتهاء، مما يضمن إنجاز العمل الذي وضعته في قائمة الانتظار في البداية.
في أعلى الشكل السابق، كان على معالِج الحدث الذي تم وضعه في قائمة انتظار بواسطة تفاعل المستخدم الانتظار لمهمة واحدة طويلة قبل أن تبدأ، ويؤدي هذا إلى تأخير حدوث التفاعل. في هذا السيناريو، ربما لاحظ المستخدم تأخرًا. في أسفل الصفحة، يمكن أن يبدأ معالج الأحداث في العمل في وقت أقرب، وقد يكون التفاعل فوريًا.
بعد أن عرفت سبب أهمية تقسيم المهام، يمكنك تعلّم كيفية تنفيذ ذلك باستخدام JavaScript.
استراتيجيات إدارة المهام
هناك نصيحة شائعة في بنية البرامج وهي تقسيم عملك إلى وظائف أصغر:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
في هذا المثال، هناك دالة اسمها saveSettings()
تستدعي خمس دوال للتحقّق من صحة نموذج وعرض مؤشر دوّار وإرسال البيانات إلى الواجهة الخلفية للتطبيق وتحديث واجهة المستخدم وإرسال الإحصاءات.
من الناحية النظرية، تم تصميم saveSettings()
بشكل جيد. إذا كنت بحاجة إلى تصحيح أخطاء إحدى هذه الدوال، فيمكنك اجتياز شجرة المشروع لمعرفة وظيفة كل دالة. يؤدي تقسيم العمل مثل هذا إلى تسهيل التنقل في المشروعات وصيانتها.
هناك مشكلة محتملة هنا، وهي أنّ JavaScript لا يشغِّل كل من هذه الدوال كمهام منفصلة لأنّه يتم تنفيذها ضمن دالة 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();
}
}
والنتيجة هي أن المهمة التي كانت واحدة في يوم واحد تم تقسيمها الآن إلى مهام منفصلة.
واجهة برمجة تطبيقات مخصصة لنظام الجدولة
تعتبر setTimeout
طريقة فعالة لتقسيم المهام، ولكن قد يكون لها عيب: عندما يتم الرجوع إلى سلسلة التعليمات الرئيسية من خلال تأجيل تشغيل الرمز في مهمة لاحقة، تتم إضافة هذه المهمة إلى نهاية قائمة الانتظار.
إذا كنت تتحكّم في كل الرموز البرمجية على صفحتك، من الممكن إنشاء أداة جدولة خاصة بك مع إمكانية تحديد أولويات المهام، ولكن لن تستخدم النصوص البرمجية التابعة لجهات خارجية أداة الجدولة. وفي الواقع، لا يمكنك منح الأولوية للعمل في مثل هذه البيئات. ويمكنك تقسيمها فقط أو الحصول على تفاعلات المستخدمين بشكل صريح.
توفّر واجهة برمجة التطبيقات 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'});
};
هنا، تتم جدولة أولوية المهام بطريقة تتيح للمهام ذات الأولوية للمتصفّح، مثل تفاعلات المستخدم، العمل بينها حسب الحاجة.
هذا مثال مبسَّط على كيفية استخدام 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.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سباش، مقدمة من أميرالي ميرهاشيميان.