استخدام عاملي الويب لتشغيل JavaScript من سلسلة التعليمات الرئيسية للمتصفِّح

يمكن أن تؤدي بنية السلسلة الفرعية إلى تحسين موثوقية تطبيقك وتجربة المستخدم بشكل كبير.

في آخر 20 عامًا، تطور الويب بشكل كبير من مستندات ثابتة تتضمّن بعض الأنماط والصور إلى تطبيقات معقدة وديناميكية. ومع ذلك، بقي هناك شيء واحد لم يتغيّر إلى حد كبير: لدينا سلسلة محادثات واحدة فقط لكل علامة تبويب في المتصفّح (مع بعض الاستثناءات) لتنفيذ عمل عرض مواقعنا الإلكترونية وتشغيل JavaScript.

ونتيجةً لذلك، أصبحت سلسلة التعليمات الرئيسية مشغولةً بشكلٍ كبير. ومع زيادة تعقيد تطبيقات الويب، يصبح الخيط الرئيسي عائقًا كبيرًا في الأداء. والأسوأ من ذلك، إنّ الوقت الذي يستغرقه تشغيل الرمز البرمجي في سلسلة المهام الرئيسية لمستخدم معيّن لا يمكن توقّعه بالكامل تقريبًا لأنّ إمكانات الجهاز لها تأثير كبير في الأداء. وستزداد هذه التقلّبات مع اتّصال المستخدمين بالويب من مجموعة متنوّعة بشكل متزايد من الأجهزة، بدءًا من الهواتف العادية ذات الأداء المحدود إلى الأجهزة الرائدة ذات الأداء العالي ومعدل التحديث العالي.

إذا أردنا أن تستوفي تطبيقات الويب المتقدّمة إرشادات الأداء بشكل موثوق، مثل مؤشرات أداء الويب الأساسية التي تستند إلى بيانات تجريبية عن الإدراك البشري وعلم النفس، نحتاج إلى طُرق لتنفيذ الرمز البرمجي خارج سلسلة المهام الرئيسية (OMT).

لماذا يجب استخدام Web Workers؟

JavaScript هي لغة تستخدم سلسلة مهام واحدة بشكل تلقائي، وهي تُنفِّذ المهام على السلسلة الرئيسية. ومع ذلك، توفّر وحدات عمل الويب نوعًا من مخرج الطوارئ من سلسلة التعليمات الرئيسية من خلال السماح للمطوّرين بإنشاء سلاسل تعليمات منفصلة لمعالجة العمل خارج سلسلة التعليمات الرئيسية. على الرغم من أنّ نطاق عمل مهام الويب محدود ولا يتيح الوصول المباشر إلى نموذج DOM، إلا أنّه يمكن أن يكون مفيدًا بشكل كبير إذا كان هناك عمل كبير يجب تنفيذه والذي قد يؤدي إلى إرباك الخيط الرئيسي.

في ما يتعلّق بمؤشرات أداء الويب الأساسية، قد يكون من المفيد تنفيذ المهام خارج سلسلة التعليمات الرئيسية. على وجه الخصوص، يمكن أن يؤدي تفريغ العمل من سلسلة المهام الرئيسية إلى عمال الويب إلى تقليل الصراع على سلسلة المهام الرئيسية، ما قد يؤدي إلى تحسين مقياس مدى استجابة الصفحة لتفاعلات المستخدم (INP). عندما يكون لدى سلسلة المحادثات الرئيسية عدد أقل من المهام التي يجب معالجتها، يمكنها الاستجابة بشكل أسرع لتفاعلات المستخدمين.

إنّ تقليل عمل سلسلة التعليمات الرئيسية، خاصةً أثناء بدء التشغيل، يُعدّ أيضًا ميزة محتملة لمقياس سرعة عرض أكبر محتوى مرئي (LCP) من خلال تقليل المهام الطويلة. يتطلّب عرض عنصر LCP وقتًا في الخيط الرئيسي، سواءً لعرض النص أو الصور، وهما عنصران شائعان من عناصر LCP. ومن خلال تقليل عمل الخيط الرئيسي بشكل عام، يمكنك التأكّد من أنّه من غير المرجّح أن يتم حظر عنصر LCP في صفحتك بسبب عمل مُكلف يمكن لعامل الويب معالجته بدلاً من ذلك.

إنشاء سلسلة محادثات باستخدام Web Workers

تتيح لك الأنظمة الأساسية الأخرى عادةً العمل بشكل موازٍ من خلال السماح لك بمنح سلسلة محادثات وظيفة يتم تنفيذها بشكل موازٍ مع بقية برنامجك. يمكنك الوصول إلى المتغيّرات نفسها من كلا الخيطَين، ويمكن مزامنة الوصول إلى هذه الموارد المشتركة باستخدام أقفال المهام المتزامنة وعلامات المرور لمنع حالات السباق.

في JavaScript، يمكننا الحصول على وظائف مشابهة تقريبًا من Web Workers، وهي وظائف متوفّرة منذ عام 2007 ومدعومة في جميع المتصفحات الرئيسية منذ عام 2012. يتم تشغيل Web workers بشكل موازٍ مع سلسلة المهام الرئيسية، ولكن بخلاف تسلسل المهام في نظام التشغيل، لا يمكنها مشاركة المتغيّرات.

لإنشاء عامل ويب، عليك تمرير ملف إلى أداة إنشاء العامل، ما يؤدي إلى بدء تشغيل هذا الملف في سلسلة محادثات منفصلة:

const worker = new Worker("./worker.js");

يمكنك التواصل مع Web Worker من خلال إرسال الرسائل باستخدام واجهة برمجة التطبيقات postMessage API. نقْل قيمة الرسالة كمَعلمة في طلب 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 API نفسها في Web Worker وإعداد مستمع للأحداث في سلسلة المحادثات الرئيسية:

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 Worker بشكل أساسي لنقل جزء واحد من العمل الثقيل خارج السلسلة الرئيسية. إنّ محاولة معالجة عمليات متعدّدة باستخدام عامل تشغيل ويب واحد يصبح أمرًا صعبًا بسرعة: عليك ترميز المَعلمات في الرسالة، بالإضافة إلى العملية، وعليك أيضًا إجراء عمليات احتساب لمطابقة الردود مع الطلبات. من المحتمل أنّ هذه التعقيدات هي السبب في عدم استخدام مهام تطبيقات الويب على نطاق أوسع.

ولكن إذا تمكّنا من إزالة بعض الصعوبات في التواصل بين سلسلة المهام الرئيسية وWeb Workers، يمكن أن يكون هذا النموذج مناسبًا تمامًا للعديد من حالات الاستخدام. لحسن الحظ، تتوفّر مكتبة توفّر لك هذه الميزة.

Comlink هي مكتبة تهدف إلى السماح لك باستخدام مهام Worker على الويب بدون التفكير في تفاصيل 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، لذا لا يمكنك وضع أجزاء من تطبيقك تعتمد على هذا الوصول في عامل. ومع ذلك، فإنّ كل قطعة صغيرة من الرمز البرمجي التي يتم نقلها إلى أحد خيوط العمل توفّر مساحة أكبر في سلسلة المهام الرئيسية للعمليات التي يجب أن تكون موجودة، مثل تعديل واجهة المستخدم.

تتمثل إحدى المشاكل التي تواجه مطوّري الويب في أنّ معظم تطبيقات الويب تعتمد على إطار عمل لواجهة المستخدم مثل Vue أو React لتنسيق كل العناصر في التطبيق، فكل عنصر هو جزء من إطار العمل وبالتالي يكون مرتبطًا بتنسيق DOM بشكلٍ أساسي. يبدو أنّ ذلك سيصعّب نقل البيانات إلى بنية OMT.

ومع ذلك، إذا انتقلنا إلى نموذج يتم فيه فصل مشاكل واجهة المستخدم عن المشاكل الأخرى، مثل إدارة الحالة، يمكن أن تكون مهام Web Worker مفيدة جدًا حتى مع التطبيقات المستندة إلى إطار عمل. وهذا هو النهج الذي تم اتّخاذه مع PROXX.

PROXX: دراسة حالة عن تحسين تجربة المستخدِم

طوَّر فريق Google Chrome لعبة PROXX كنسخة طبق الأصل من لعبة "ألغام بحرية" تستوفي متطلبات تطبيقات الويب التقدّمية، بما في ذلك العمل بلا اتصال بالإنترنت وتوفير تجربة تفاعلية للمستخدمين. لسوء الحظ، كان أداء الإصدارات الأولى من اللعبة ضعيفًا على الأجهزة ذات الإمكانيات المحدودة، مثل الهواتف المزوّدة بميزات أساسية، ما دفع الفريق إلى إدراك أنّ الخيط الرئيسي كان يشكّل عائقًا.

قرّر الفريق استخدام مهام Web Worker لفصل الحالة المرئية للّعبة عن منطقها:

  • تعالج سلسلة التعليمات الرئيسية عرض الرسوم المتحركة وتأثيرات الانتقال.
  • يعالج Web Worker منطق اللعبة، وهو حسابي بحت.

كان لاختبار OMT تأثيرات مثيرة للاهتمام في أداء هاتف PROXX المزوّد بميزات أساسية. في الإصدار غير المزوّد بميزة "التتبّع الدقيق للمتسوّقين"، يتم تجميد واجهة المستخدم لمدة ست ثوانٍ بعد تفاعل المستخدم معها. لا تتوفّر أيّ ملاحظات، وعلى المستخدم الانتظار لمدة ست ثوانٍ كاملة قبل أن يتمكّن من تنفيذ إجراء آخر.

وقت استجابة واجهة المستخدم في إصدار غير مزوّد بميزة OMT من PROXX

في إصدار OMT، تستغرق اللعبة اثنتي عشرة ثانية لإكمال تعديل واجهة المستخدم. على الرغم من أنّ ذلك يبدو وكأنه خسارة في الأداء، إلا أنّه يؤدي في الواقع إلى زيادة الملاحظات التي يقدّمها المستخدمون. يحدث التباطؤ لأنّ التطبيق يرسل عددًا أكبر من اللقطات مقارنةً بالإصدار غير المزوّد بميزة "التتبّع بالاستناد إلى علامات مائية"، والذي لا يرسل أي لقطات على الإطلاق. وبالتالي، يدرك المستخدم أنّ هناك عملية جارية ويمكنه مواصلة اللعب أثناء تعديل واجهة المستخدم، ما يجعل تجربة اللعب أفضل بكثير.

وقت استجابة واجهة المستخدم في إصدار OMT من PROXX

نهدف من خلال ذلك إلى تحقيق موازنة مدروسة: نمنح مستخدمي الأجهزة ذات الإمكانيات المحدودة تجربة تبدو أفضل بدون معاقبة مستخدمي الأجهزة الراقية.

الآثار المترتبة على بنية OMT

كما يوضّح مثال PROXX، تسمح OMT بتشغيل تطبيقك بشكل موثوق على نطاق أوسع من الأجهزة، ولكنّها لا تجعله أسرع:

  • ما تفعله هو نقل العمل من سلسلة المهام الرئيسية، وليس تقليل العمل.
  • يمكن أن تؤدي الإجراءات الإضافية المطلوبة للتواصل بين Web Worker والسلسلة الرئيسية أحيانًا إلى إبطاء الأداء بشكل طفيف.

مراعاة ميزات الملحقات وعيوبها

بما أنّ سلسلة التعليمات الرئيسية تكون متاحة لمعالجة تفاعلات المستخدمين، مثل الانتقال للأعلى أو للأسفل أثناء تشغيل JavaScript، يكون عدد اللقطات التي يتم إسقاطها أقلّ، حتى لو كان إجمالي وقت الانتظار أطول قليلاً. من الأفضل أن ينتظر المستخدم قليلاً بدلاً من حذف إطار لأنّ هامش الخطأ يكون أصغر في حالة حذف الإطارات: يحدث حذف الإطارات في مللي ثانية، بينما لديك مئات من المللي ثانية قبل أن يلاحظ المستخدم وقت الانتظار.

بسبب عدم إمكانية توقّع الأداء على جميع الأجهزة، يهدف تصميم OMT إلى تقليل المخاطر، أي جعل تطبيقك أكثر ثباتًا في مواجهة ظروف التشغيل المتغيرة للغاية، وليس الاستفادة من مزايا الأداء الناتجة عن التوازي. إنّ الزيادة في المرونة والتحسينات على تجربة المستخدم تستحقّ أكثر من أيّ تنازل صغير في السرعة.

ملاحظة حول الأدوات

لا تُستخدم وحدات Web Worker بشكل شائع بعد، لذا لا تتيح معظم أدوات الوحدات استخدامها بشكل تلقائي، مثل webpack وRollup. (يمكن استخدام Parcel). لحسن الحظ، تتوفّر مكوّنات إضافية لجعل عمال الويب يعملون مع webpack وRollup:

ملخّص

لضمان أن تكون تطبيقاتنا موثوقة وسهلة الاستخدام قدر الإمكان، خاصةً في السوق المتزايدة العولمة، نحتاج إلى إتاحة استخدامها على الأجهزة ذات الإمكانيات المحدودة، لأنّها الطريقة التي يستخدمها معظم المستخدمين للوصول إلى الويب في جميع أنحاء العالم. توفّر OMT طريقة واعدة لتحسين الأداء على هذه الأجهزة بدون التأثير سلبًا في مستخدمي الأجهزة الراقية.

توفّر OMT أيضًا مزايا ثانوية:

لا يجب أن يكون عمال الويب مخيفين. تُسهّل أدوات مثل Comlink عمل الموظفين وتجعلها خيارًا مناسبًا لمجموعة كبيرة من تطبيقات الويب.