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

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

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

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

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

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

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

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

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

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

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

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

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

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 Workers، يمكن أن يكون هذا النموذج مناسبًا تمامًا للعديد من حالات الاستخدام. لحسن الحظ، تتوفّر مكتبة توفّر لك هذه الميزة.

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

ما هو الرمز الذي يجب نقله إلى Web Worker؟

لا يمكن للعاملين على الويب الوصول إلى نموذج العناصر في المستند (DOM) والعديد من واجهات برمجة التطبيقات، مثل WebUSB، أو WebRTC، أو Web Audio، وبالتالي لا يمكن تشغيل أي جزء من التطبيق. ومع ذلك، فإنّ كل قطعة صغيرة من الرمز البرمجي التي يتم نقلها إلى أحد خيوط العمل توفّر مساحة أكبر في سلسلة المهام الرئيسية للعمليات التي يجب أن تكون موجودة، مثل تعديل واجهة المستخدم.

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

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

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

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

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

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

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

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

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

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

هذا الأمر مجرد خيار للمفاضلة الواعية، فنحن نمنح مستخدمي الأجهزة المحظورة تجربة تشعر أفضل بدون فرض عقوبات على مستخدمي الأجهزة المتطورة.

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

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

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

مراعاة الإيجابيات والسلبيات

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

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

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

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

ملخّص

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

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

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