يمكن أن يؤدي استخدام بنية غير متوافقة مع سلسلة التعليمات الرئيسية إلى تحسين موثوقية تطبيقك وتجربة المستخدم بشكل كبير.
على مدار الـ 20 عامًا الماضية، تطوّر الويب بشكل كبير من مستندات ثابتة تتضمّن بعض الأنماط والصور إلى تطبيقات معقّدة وديناميكية. ومع ذلك، ظلّ هناك شيء واحد لم يتغيّر إلى حد كبير، وهو أنّ لدينا سلسلة محادثات واحدة فقط لكل علامة تبويب في المتصفّح (مع بعض الاستثناءات) لتنفيذ عملية عرض مواقعنا الإلكترونية وتشغيل JavaScript.
نتيجةً لذلك، أصبحت سلسلة التعليمات الرئيسية مثقلةً بشكل كبير. ومع ازدياد تعقيد تطبيقات الويب، يصبح مؤشر الترابط الرئيسي عاملاً مهمًا يعيق الأداء. والأسوأ من ذلك، أنّ مقدار الوقت الذي يستغرقه تنفيذ الرمز البرمجي في سلسلة التعليمات الرئيسية لمستخدم معيّن لا يمكن توقّعه تقريبًا لأنّ إمكانات الجهاز تؤثر بشكل كبير في الأداء. ولن يزداد هذا التفاوت إلا مع وصول المستخدمين إلى الويب من مجموعة متنوعة بشكل متزايد من الأجهزة، بدءًا من الهواتف العادية ذات الإمكانات المحدودة جدًا إلى الأجهزة الرائدة ذات الأداء العالي ومعدل التحديث العالي.
إذا أردنا أن تستوفي تطبيقات الويب المتطورة إرشادات الأداء بشكل موثوق، مثل مؤشرات Core Web Vitals المستندة إلى بيانات تجريبية حول الإدراك وعلم النفس البشري، نحتاج إلى طرق لتنفيذ الرمز خارج سلسلة التعليمات الرئيسية.
لماذا نستخدم Web Workers؟
إنّ JavaScript هي لغة ذات سلسلة تعليمات واحدة بشكلٍ تلقائي، وتنفّذ المهام على سلسلة التعليمات الرئيسية. ومع ذلك، توفّر Web Workers نوعًا من مخرج الطوارئ من سلسلة التعليمات الرئيسية من خلال السماح للمطوّرين بإنشاء سلاسل تعليمات منفصلة للتعامل مع العمل خارج سلسلة التعليمات الرئيسية. على الرغم من أنّ نطاق Web Workers محدود ولا يتيح الوصول المباشر إلى نموذج 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);
});
من الواضح أنّ هذا النهج محدود إلى حدّ ما. في السابق، كان يتم استخدام Web Workers بشكل أساسي لنقل جزء واحد من العمل الثقيل خارج السلسلة الرئيسية. إنّ محاولة تنفيذ عمليات متعددة باستخدام عامل ويب واحد تصبح غير عملية بسرعة: عليك ترميز المَعلمات والعملية في الرسالة، وعليك إجراء عمليات حفظ بيانات لمطابقة الردود مع الطلبات. من المحتمل أنّ هذا التعقيد هو السبب في عدم استخدام Web Workers على نطاق أوسع.
ولكن إذا أمكننا إزالة بعض الصعوبات في التواصل بين سلسلة التعليمات الرئيسية وWeb Workers، يمكن أن يكون هذا النموذج مناسبًا للعديد من حالات الاستخدام. لحسن الحظ، تتوفّر مكتبة تفعل ذلك بالضبط.
Comlink: تسهيل استخدام عاملي الويب
Comlink هي مكتبة تهدف إلى السماح لك باستخدام برامج Web Worker بدون الحاجة إلى التفكير في تفاصيل postMessage. تتيح لك Comlink مشاركة المتغيرات بين عاملي الويب والسلسلة الرئيسية، تمامًا مثل لغات البرمجة الأخرى التي تتيح استخدام سلاسل التعليمات البرمجية.
يمكنك إعداد Comlink من خلال استيراده في عامل ويب وتحديد مجموعة من الدوال لعرضها في سلسلة التعليمات الرئيسية. بعد ذلك، يمكنك استيراد 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 في سلسلة التعليمات الرئيسية بالطريقة نفسها التي يتصرف بها في عامل الويب، باستثناء أنّ كل دالة تعرض وعدًا بقيمة بدلاً من القيمة نفسها.
ما هو الرمز الذي يجب نقله إلى عامل ويب؟
لا يمكن للعاملين على الويب الوصول إلى نموذج المستند (DOM) والعديد من واجهات برمجة التطبيقات، مثل WebUSB أو WebRTC أو Web Audio، لذا لا يمكنك وضع أجزاء من تطبيقك تعتمد على هذا الوصول في عامل. ومع ذلك، فإنّ كل جزء صغير من الرمز البرمجي يتم نقله إلى عامل يوفّر مساحة أكبر على سلسلة التعليمات الرئيسية للأشياء التي يجب أن تكون موجودة، مثل تعديل واجهة المستخدم.
تتمثّل إحدى المشاكل التي يواجهها مطوّرو الويب في أنّ معظم تطبيقات الويب تعتمد على إطار عمل لواجهة المستخدم، مثل Vue أو React، لتنظيم كل شيء في التطبيق، فكل شيء هو أحد مكونات إطار العمل وبالتالي يرتبط بشكل أساسي بنموذج المستند (DOM). يبدو أنّ ذلك سيصعّب عملية نقل البيانات إلى بنية OMT.
ومع ذلك، إذا انتقلنا إلى نموذج يتم فيه فصل مخاوف واجهة المستخدم عن المخاوف الأخرى، مثل إدارة الحالة، يمكن أن تكون أدوات Web Workers مفيدة جدًا حتى مع التطبيقات المستندة إلى الأُطر. هذا هو الأسلوب الذي اتّبعناه بالضبط في لعبة PROXX.
PROXX: دراسة حالة حول "التحسين على الأجهزة الجوّالة"
طوّر فريق Google Chrome لعبة PROXX كنسخة طبق الأصل من لعبة Minesweeper تستوفي متطلبات تطبيقات الويب التقدّمية، بما في ذلك إمكانية تشغيلها بلا إنترنت وتوفير تجربة مستخدم جذابة. لسوء الحظ، لم تحقّق الإصدارات الأولى من اللعبة أداءً جيدًا على الأجهزة ذات الموارد المحدودة، مثل الهواتف العادية، ما دفع الفريق إلى إدراك أنّ سلسلة التعليمات الرئيسية كانت تشكّل عنق الزجاجة.
قرّر الفريق استخدام Web Workers لفصل الحالة المرئية للعبة عن منطقها:
- تتعامل سلسلة التعليمات الرئيسية مع عرض الحركات وتأثيرات الانتقال.
- يتولّى عامل الويب معالجة منطق اللعبة، وهو عملية حسابية بحتة.
كانت ميزة "التحسين أثناء التنقل" لها تأثيرات مثيرة للاهتمام على أداء تطبيق PROXX على الهواتف العادية. في الإصدار غير المتوافق مع OMT، يتم تجميد واجهة المستخدم لمدة ست ثوانٍ بعد أن يتفاعل المستخدم معها. لا تظهر أي ملاحظات، وعلى المستخدم الانتظار لمدة ست ثوانٍ كاملة قبل أن يتمكّن من فعل أي شيء آخر.
في إصدار OMT، تستغرق اللعبة اثنتي عشرة ثانية لإكمال تحديث واجهة المستخدم. وعلى الرغم من أنّ ذلك يبدو وكأنّه يؤدي إلى انخفاض الأداء، إلا أنّه يؤدي في الواقع إلى زيادة الملاحظات المقدَّمة للمستخدم. يحدث التباطؤ لأنّ التطبيق يرسل عددًا من اللقطات أكبر من إصدار OMT، الذي لا يرسل أي لقطات على الإطلاق. وبالتالي، يعلم المستخدم أنّ هناك إجراءً يتم تنفيذه ويمكنه مواصلة اللعب أثناء تعديل واجهة المستخدم، ما يجعل تجربة اللعب أفضل بكثير.
هذا حلّ مدروس: نحن نقدّم لمستخدمي الأجهزة ذات الموارد المحدودة تجربة تبدو أفضل بدون معاقبة مستخدمي الأجهزة المتطورة.
تداعيات بنية OMT
كما يوضّح مثال PROXX، تتيح ميزة "التحسين أثناء التنفيذ" تشغيل تطبيقك بشكل موثوق على مجموعة أكبر من الأجهزة، ولكنّها لا تجعل تطبيقك أسرع:
- أنت تنقل العمل من سلسلة التعليمات الرئيسية فقط، ولا تقلّل من حجم العمل.
- يمكن أن يؤدي عبء الاتصال الإضافي بين Web Worker وسلسلة التعليمات الرئيسية أحيانًا إلى إبطاء العملية بشكل طفيف.
التفكير في الإيجابيات والسلبيات
بما أنّ سلسلة التعليمات الرئيسية يمكنها معالجة تفاعلات المستخدمين، مثل التمرير، أثناء تشغيل JavaScript، يكون هناك عدد أقل من اللقطات التي تم إسقاطها على الرغم من أنّ إجمالي وقت الانتظار قد يكون أطول قليلاً. من الأفضل أن ينتظر المستخدم قليلاً بدلاً من إسقاط إطار، لأنّ هامش الخطأ أصغر بالنسبة إلى الإطارات التي تم إسقاطها: يحدث إسقاط الإطار في غضون أجزاء من الثانية، بينما يكون لديك مئات الأجزاء من الثانية قبل أن يدرك المستخدم وقت الانتظار.
بسبب عدم القدرة على التنبؤ بالأداء على جميع الأجهزة، يهدف تصميم "التنفيذ المتزامن على أجهزة متعددة" إلى تقليل المخاطر، أي جعل تطبيقك أكثر قوة في مواجهة ظروف وقت التشغيل المتغيرة بشكل كبير، وليس إلى تحقيق مزايا الأداء من خلال التنفيذ المتوازي. إنّ زيادة المرونة والتحسينات في تجربة المستخدم تستحقّ أكثر من أيّ تنازل بسيط في السرعة.
ملاحظة حول الأدوات
لم تصبح Web Workers شائعة بعد، لذا لا تتيح معظم أدوات الوحدات، مثل webpack وRollup، استخدامها مباشرةً. (Parcel) لحسن الحظ، تتوفّر مكوّنات إضافية تتيح عمل Web Workers مع webpack وRollup:
- worker-plugin لـ webpack
- rollup-plugin-off-main-thread لـ Rollup
في الختام
لضمان توفير تطبيقاتنا بأكبر قدر ممكن من الموثوقية وإمكانية الوصول إليها، لا سيما في سوق يزداد عولمةً، علينا توفير الدعم للأجهزة ذات الموارد المحدودة، لأنّها الطريقة التي يصل بها معظم المستخدمين إلى الويب في جميع أنحاء العالم. توفّر ميزة "التحسين أثناء التنقل" طريقة واعدة لزيادة الأداء على هذه الأجهزة بدون التأثير سلبًا في مستخدمي الأجهزة المتطورة.
بالإضافة إلى ذلك، تقدّم OMT مزايا ثانوية:
- تنقل هذه السمة تكاليف تنفيذ JavaScript إلى سلسلة محادثات منفصلة.
- يؤدي ذلك إلى نقل تكاليف التحليل، ما يعني أنّ واجهة المستخدم قد يتم تشغيلها بشكل أسرع. قد يؤدي ذلك إلى تقليل سرعة عرض المحتوى على الصفحة أو حتى وقت الاستجابة، ما قد يؤدي بدوره إلى زيادة نتيجة Lighthouse.
لا داعي للقلق بشأن Web Workers. تساعد أدوات مثل Comlink في تسهيل عمل المشغّلات وتجعلها خيارًا مناسبًا لمجموعة كبيرة من تطبيقات الويب.