لقد تمّ إبلاغك بأنّه "يجب عدم حظر سلسلة التعليمات الرئيسية" و"تقسيم المهام الطويلة"، ولكن ما المقصود بتنفيذ هذه الإجراءات؟
تتلخّص النصائح الشائعة للحفاظ على سرعة تطبيقات 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()
لتشغيل ثلاث مهام بأعلى أولوية ممكنة، والمهمتَين المتبقيتَين بأدنى أولوية ممكنة.
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()
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()
في الاستمرار، ما يعني أنّه في حال إيقاف المعالجة في منتصف مجموعة من المهام، ستستمر المهام المُجدوَلة الأخرى بالترتيب نفسه بعد نقطة الإيقاف. يؤدي ذلك إلى منع الرموز البرمجية من النصوص البرمجية التابعة لجهات خارجية من مقاطعة ترتيب تنفيذ الرمز البرمجي.
عدم استخدام isInputPending()
توفّر واجهة برمجة التطبيقات isInputPending()
طريقة للتحقّق مما إذا حاول أحد المستخدِمين التفاعل مع صفحة، ولا تُعرِض أيّ نتائج إلا إذا كانت هناك بيانات في انتظار المراجعة.
يتيح ذلك لـ JavaScript مواصلة العمل في حال عدم توفّر أي إدخالات معلّقة، بدلاً من التوقّف والانتقال إلى نهاية قائمة المهام. ويمكن أن يؤدّي ذلك إلى تحسينات مُبهرة في الأداء، كما هو موضّح بالتفصيل في Intent to Ship، وذلك للمواقع الإلكترونية التي قد لا تعود إلى سلسلة المحادثات الرئيسية بخلاف ذلك.
ومع ذلك، منذ إطلاق واجهة برمجة التطبيقات هذه، زاد فهمنا للأداء، لا سيما مع طرح نموذج INP. لم نعُد نقترح استخدام واجهة برمجة التطبيقات هذه، وننصحك بدلاً من ذلك بتقديم البيانات بغض النظر عمّا إذا كانت البيانات في انتظار المراجعة أم لا لعدة أسباب:
- قد يعرض
isInputPending()
خطأً القيمةfalse
على الرغم من تفاعل المستخدِم في بعض الحالات. - الإدخال ليس الحالة الوحيدة التي يجب أن تؤدي فيها المهام إلى نتيجة. يمكن أن تكون الصور المتحركة وغيرها من التعديلات العادية على واجهة المستخدم مهمة بنفس القدر لتوفير صفحة ويب متجاوبة.
- ومنذ ذلك الحين، تمّت إضافة واجهات برمجة تطبيقات أكثر شمولية تعالج المخاوف المتعلّقة بتحقيق الربح، مثل
scheduler.postTask()
وscheduler.yield()
.
الخاتمة
إنّ إدارة المهام أمر صعب، ولكنّ اتّباع هذه الخطوات يضمن استجابة صفحتك بشكل أسرع لتفاعلات المستخدمين. لا تتوفّر نصيحة واحدة لإدارة المهام وتحديد أولوياتها، بل هناك عدد من الأساليب المختلفة. للتأكيد مرة أخرى، هذه هي الأشياء الأساسية التي ستحتاج إلى وضعها في الاعتبار عند إدارة المهام:
- يجب التنازل عن السلسلة الرئيسية للمهام المهمة الموجَّهة للمستخدمين.
- تحديد أولويات المهام باستخدام
postTask()
- ننصحك بتجربة "
scheduler.yield()
". - أخيرًا، ابذل أقل قدر ممكن من الجهد في دوالّك.
باستخدام واحدة أو أكثر من هذه الأدوات، يُفترض أن تكون قادرًا على تنظيم العمل في تطبيقك بحيث يعطي الأولوية لاحتياجات المستخدم، مع ضمان إنجاز العمل الأقل أهمية. سيؤدي ذلك إلى توفير تجربة أفضل للمستخدمين تتسم بوقت استجابة أسرع وسهولة أكبر في الاستخدام.
نشكر فيليب والتون على الفحص الفني لهذا الدليل.
صورة مصغّرة مصدرها Unsplash، من إبداع Amirali Mirhashemian.