يمكن أن تؤدي البنية التي لا تعتمد على سلسلة التعليمات الرئيسية إلى تحسين موثوقية تطبيقك وتجربة المستخدم بشكل كبير.
على مدار العشرين عامًا الماضية، تطوّر الويب بشكل كبير من مستندات ثابتة تتضمّن بعض الأنماط والصور إلى تطبيقات معقّدة وديناميكية. ومع ذلك، ظلّ هناك أمر واحد لم يتغيّر إلى حد كبير: لدينا سلسلة تعليمات واحدة فقط لكل علامة تبويب متصفّح (مع بعض الاستثناءات) لتنفيذ مهمة عرض مواقعنا الإلكترونية وتشغيل JavaScript.
نتيجةً لذلك، أصبحت سلسلة التعليمات الرئيسية مثقلة بالمهام بشكل كبير. ومع زيادة تعقيد تطبيقات الويب، يصبح سلسلة التعليمات الرئيسية عائقًا كبيرًا أمام الأداء. والأسوأ من ذلك أنّ مقدار الوقت الذي يستغرقه تشغيل الرمز البرمجي في سلسلة التعليمات الرئيسية لمستخدم معيّن لا يمكن توقّعه بشكل كامل تقريبًا لأنّ إمكانات الجهاز لها تأثير كبير في الأداء. وستزداد صعوبة التنبؤ مع وصول المستخدمين إلى الويب من مجموعة متنوعة من الأجهزة، بدءًا من الهواتف العادية المحدودة الإمكانات إلى الأجهزة الرائدة العالية الأداء ذات معدّل التحديث المرتفع.
إذا أردنا أن تستوفي تطبيقات الويب المتطوّرة إرشادات الأداء بشكل موثوق، مثل Core Web Vitals، والتي تستند إلى بيانات تجريبية حول الإدراك البشري وعلم النفس، نحتاج إلى طرق لتنفيذ الرمز البرمجي خارج سلسلة التعليمات الرئيسية (OMT).
ما أهمية Web Workers؟
JavaScript هي لغة أحادية السلسلة بشكل تلقائي، وتنفّذ المهام على سلسلة التعليمات الرئيسية. ومع ذلك، توفّر مشغِّلات الويب نوعًا من الملاذ الآمن من سلسلة التعليمات الرئيسية من خلال السماح للمطوّرين بإنشاء سلاسل تعليمات منفصلة للتعامل مع العمل خارج سلسلة التعليمات الرئيسية. على الرغم من أنّ نطاق مشغّلي الويب محدود ولا يوفّر وصولاً مباشرًا إلى نموذج DOM، يمكن أن يكون مفيدًا للغاية إذا كان هناك عمل كبير يجب إنجازه، وإلا سيؤدي إلى إرباك سلسلة التعليمات الرئيسية.
في ما يتعلق بمؤشرات Core Web Vitals، يمكن أن يكون تنفيذ العمل خارج سلسلة التعليمات الرئيسية مفيدًا. على وجه الخصوص، يمكن أن يؤدي نقل العمل من سلسلة التعليمات الرئيسية إلى مشغّلي الويب إلى تقليل التنافس على سلسلة التعليمات الرئيسية، ما قد يؤدي إلى تحسين مقياس استجابة مدى استجابة الصفحة لتفاعلات المستخدم (INP). عندما يكون لدى سلسلة التعليمات الرئيسية عمل أقل لمعالجته، يمكنها الاستجابة بسرعة أكبر لتفاعلات المستخدمين.
إنّ تقليل العمل في سلسلة التعليمات الرئيسية، خاصةً أثناء بدء التشغيل، يحقّق أيضًا فائدة محتملة لمقياس سرعة عرض أكبر محتوى مرئي (LCP) من خلال تقليل المهام التي يستغرق تنفيذها وقتًا طويلاً. يتطلّب عرض عنصر LCP وقتًا في سلسلة التعليمات الرئيسية، سواء لعرض النصوص أو الصور، وهي عناصر LCP متكرّرة وشائعة. ومن خلال تقليل عمل سلسلة التعليمات الرئيسية بشكل عام، يمكنك التأكّد من أنّ عنصر LCP في صفحتك أقل عرضة للحظر بسبب العمل المكلف الذي يمكن أن يتعامل معه مشغّل الويب بدلاً من ذلك.
المعالجة المتعدّدة باستخدام Web Workers
تتيح المنصات الأخرى عادةً العمل الموازي من خلال السماح لك بإعطاء سلسلة مهام دالة، والتي تعمل بالتوازي مع بقية برنامجك. يمكنك الوصول إلى المتغيرات نفسها من كلا السلسلتين، ويمكن مزامنة الوصول إلى هذه الموارد المشتركة مع الاستبعادات المتبادلة والإشارات لمنع حالات السباق.
في JavaScript، يمكننا الحصول على وظائف مشابهة تقريبًا من Web Workers، والتي تم توفيرها منذ عام 2007 وأصبحت متوافقة مع جميع المتصفحات الرئيسية منذ عام 2012. يتم تشغيل Web Workers بالتوازي مع سلسلة التعليمات الرئيسية، ولكن على عكس سلاسل التعليمات في نظام التشغيل، لا يمكنها مشاركة المتغيّرات.
لإنشاء عامل ويب، مرِّر ملفًا إلى دالة الإنشاء الخاصة بالعامل، والتي تبدأ تشغيل هذا الملف في سلسلة محادثات منفصلة:
const worker = new Worker("./worker.js");
يمكنك التواصل مع مشغّل الويب من خلال إرسال رسائل باستخدام واجهة برمجة التطبيقات postMessage. مرِّر قيمة الرسالة كمَعلمة في طلب postMessage، ثم أضِف متتبِّع أحداث الرسائل إلى العامل:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
// ...
});
لإرسال رسالة إلى سلسلة المحادثات الرئيسية، استخدِم واجهة برمجة التطبيقات postMessage نفسها في مشغّل الويب وأعِدّ متتبِّع أحداث في سلسلة المحادثات الرئيسية:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
console.log(event.data);
});
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
postMessage(a + b);
});
صحيح أنّ هذا النهج محدود إلى حدّ ما. في السابق، كان يتم استخدام مشغّلات الويب بشكل أساسي لنقل جزء واحد من العمل الثقيل خارج سلسلة التعليمات الرئيسية. تصبح محاولة التعامل مع عمليات متعددة باستخدام عامل ويب واحد غير عملية بسرعة: عليك ترميز ليس فقط المعلمات ولكن أيضًا العملية في الرسالة، وعليك إجراء مسك الدفاتر لمطابقة الردود مع الطلبات. ومن المرجّح أنّ هذا التعقيد هو السبب في عدم استخدام مشغّلي الويب على نطاق أوسع.
ولكن إذا تمكّنا من إزالة بعض الصعوبات في التواصل بين سلسلة التعليمات الرئيسية وعاملي الويب، يمكن أن يكون هذا النموذج مناسبًا جدًا للعديد من حالات الاستخدام. ولحسن الحظ، هناك مكتبة تقوم بذلك بالضبط!
Comlink: تبسيط استخدام مشغِّلات Web Workers
Comlink هي مكتبة تهدف إلى السماح لك باستخدام Web Workers بدون الحاجة إلى التفكير في تفاصيل postMessage. تتيح لك Comlink مشاركة المتغيّرات بين مشغّلي الويب والمؤشر الرئيسي، مثل لغات البرمجة الأخرى التي تتيح استخدام سلاسل المحادثات.
يمكنك إعداد Comlink من خلال استيرادها في Web Worker وتحديد مجموعة من الدوال لعرضها في سلسلة التعليمات الرئيسية. بعد ذلك، يمكنك استيراد Comlink في سلسلة التعليمات الرئيسية، وتضمين العامل، والوصول إلى الدوال المعروضة:
worker.js
import {expose} from 'comlink';
const api = {
someMethod() {
// ...
}
}
expose(api);
main.js
import {wrap} from 'comlink';
const worker = new Worker('./worker.js');
const api = wrap(worker);
يتصرف المتغير api في سلسلة التعليمات الرئيسية بالطريقة نفسها التي يتصرف بها المتغير في مشغّل الويب، باستثناء أنّ كل دالة تعرض وعدًا بقيمة بدلاً من القيمة نفسها.
ما هو الرمز البرمجي الذي يجب نقله إلى Web Worker؟
لا يمكن لبرامج Web Worker الوصول إلى نموذج DOM والعديد من واجهات برمجة التطبيقات، مثل WebUSB أو WebRTC أو Web Audio، لذا لا يمكنك وضع أجزاء من تطبيقك تعتمد على هذا الوصول في برنامج Worker. ومع ذلك، فإنّ كل جزء صغير من الرمز البرمجي يتم نقله إلى عامل يوفّر مساحة أكبر في سلسلة التعليمات الرئيسية للأشياء التي يجب أن تكون هناك، مثل تحديث واجهة المستخدم.
من المشاكل التي يواجهها مطوّرو تطبيقات الويب أنّ معظم تطبيقات الويب تعتمد على إطار عمل لواجهة المستخدم، مثل Vue أو React، لتنظيم كل شيء في التطبيق، فكل شيء هو مكوّن من إطار العمل، وبالتالي يرتبط بطبيعته بنموذج DOM. قد يبدو أنّ ذلك يجعل من الصعب نقل البيانات إلى بنية OMT.
ومع ذلك، إذا انتقلنا إلى نموذج يتم فيه فصل مخاوف واجهة المستخدم عن المخاوف الأخرى، مثل إدارة الحالة، يمكن أن تكون برامج الويب مفيدة جدًا حتى مع التطبيقات المستندة إلى إطار العمل. هذا هو النهج المتّبع في لعبة PROXX.
PROXX: دراسة حالة حول OMT
طوّر فريق Google Chrome لعبة PROXX كنسخة من لعبة Minesweeper تستوفي متطلبات تطبيقات الويب التقدّمية، بما في ذلك العمل بلا إنترنت وتوفير تجربة مستخدم جذابة. للأسف، كان أداء الإصدارات الأولى من اللعبة ضعيفًا على الأجهزة ذات الإمكانات المحدودة، مثل الهواتف العادية، ما جعل الفريق يدرك أنّ سلسلة التعليمات الرئيسية كانت تمثّل عقبة.
قرّر الفريق استخدام Web Workers لفصل الحالة المرئية للعبة عن منطقها:
- تتولّى سلسلة التعليمات الرئيسية عرض الصور المتحركة والانتقالات.
- يتولّى عامل الويب معالجة منطق اللعبة، وهو حسابي بحت.
كان لـ OMT تأثيرات مثيرة للاهتمام على أداء الهواتف العادية في PROXX. في الإصدار الذي لا يستخدم OMT، تتوقف واجهة المستخدم عن الاستجابة لمدة ست ثوانٍ بعد تفاعل المستخدم معها. لا تظهر أي ملاحظات، وعلى المستخدم الانتظار لمدة ست ثوانٍ كاملة قبل أن يتمكّن من تنفيذ إجراء آخر.
ومع ذلك، في إصدار OMT، تستغرق اللعبة اثنتي عشرة ثانية لإكمال تحديث واجهة المستخدم. قد يبدو ذلك وكأنّه تراجع في الأداء، ولكنّه في الواقع يؤدي إلى زيادة الملاحظات التي يتلقّاها المستخدم. يحدث هذا البطء لأنّ التطبيق يرسل عددًا أكبر من اللقطات مقارنةً بالإصدار الذي لا يستخدم OMT، والذي لا يرسل أي لقطات على الإطلاق. وبالتالي، يعرف المستخدم أنّ هناك إجراءً يتم تنفيذه ويمكنه مواصلة اللعب أثناء تحديث واجهة المستخدم، ما يجعل تجربة اللعب أفضل بكثير.
هذه مقايضة واعية: نمنح مستخدمي الأجهزة المحدودة تجربة تبدو أفضل بدون معاقبة مستخدمي الأجهزة المتطورة.
تأثيرات بنية OMT
كما يوضّح مثال PROXX، يجعل OMT تطبيقك يعمل بشكل موثوق على مجموعة أكبر من الأجهزة، ولكنه لا يجعل تطبيقك أسرع:
- أنت تنقل العمل من سلسلة التعليمات الرئيسية فقط، ولا تقلّل من العمل.
- يمكن أن يؤدي عبء الاتصال الإضافي بين مشغّل الويب والمؤشر الرئيسي إلى إبطاء الأمور بشكل طفيف في بعض الأحيان.
مراعاة الإيجابيات والسلبيات
بما أنّ سلسلة التعليمات الرئيسية تكون متاحة لمعالجة تفاعلات المستخدمين، مثل الانتقال للأعلى أو للأسفل أثناء تشغيل JavaScript، يتم إسقاط عدد أقل من اللقطات على الرغم من أنّ إجمالي وقت الانتظار قد يكون أطول قليلاً. من الأفضل جعل المستخدم ينتظر قليلاً بدلاً من إسقاط إطار لأنّ هامش الخطأ يكون أصغر في الإطارات التي تم إسقاطها: يحدث إسقاط الإطار في غضون أجزاء من الثانية، بينما لديك مئات الأجزاء من الثانية قبل أن يلاحظ المستخدم وقت الانتظار.
بسبب عدم إمكانية توقّع الأداء على جميع الأجهزة، فإنّ الهدف من بنية OMT هو الحدّ من المخاطر، أي جعل تطبيقك أكثر فعالية في مواجهة ظروف وقت التشغيل المتغيّرة للغاية، وليس تحقيق مزايا الأداء من خلال الموازاة. إنّ زيادة المرونة والتحسينات في تجربة المستخدم تستحق أي مقايضة صغيرة في السرعة.
ملاحظة حول الأدوات
لا تزال Web workers غير شائعة، لذا فإنّ معظم أدوات الوحدات، مثل webpack وRollup، لا تتيح استخدامها تلقائيًا. (Parcel يتيح ذلك) لحسن الحظ، تتوفّر مكوّنات إضافية لجعل مهام الويب تعمل مع webpack وRollup:
- worker-plugin لـ webpack
- rollup-plugin-off-main-thread لـ Rollup
الخلاصة
للتأكّد من أنّ تطبيقاتنا موثوقة ويمكن الوصول إليها قدر الإمكان، لا سيما في سوق يتزايد طابعها العالمي، نحتاج إلى توفير الدعم للأجهزة المحدودة الموارد، فهي الطريقة التي يصل بها معظم المستخدمين إلى الويب على مستوى العالم. توفّر ميزة OMT طريقة واعدة لتحسين الأداء على هذه الأجهزة بدون التأثير سلبًا في مستخدمي الأجهزة المتطوّرة.
بالإضافة إلى ذلك، يوفّر العلاج اليدوي لتقويم العظام مزايا ثانوية:
- فهي تنقل تكاليف تنفيذ JavaScript إلى سلسلة منفصلة.
- فهي تنقل تكاليف التحليل اللغوي، ما يعني أنّ واجهة المستخدم قد يتم تشغيلها بشكل أسرع. قد يؤدي ذلك إلى تقليل سرعة عرض أول محتوى مرئي أو حتى سرعة الاستعداد للتفاعل، ما قد يؤدي بدوره إلى زيادة نتيجة Lighthouse.
لا داعي للقلق بشأن مشغِّلات الويب. تتولى أدوات مثل Comlink تنفيذ العمل نيابةً عن مشغّلات الويب وتجعلها خيارًا قابلاً للتطبيق لمجموعة كبيرة من تطبيقات الويب.