اكتشاف ميزة WebAssembly

تعرَّف على كيفية استخدام أحدث ميزات WebAssembly مع توفير الدعم للمستخدمين على جميع المتصفّحات.

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

تعمل بعض الميزات الجديدة على تحسين حجم الرمز البرمجي من خلال إضافة تعليمات جديدة للعمليات الشائعة، وتعمل بعض الميزات الأخرى على إضافة عناصر أساسية فعّالة للأداء، وتعمل ميزات أخرى على تحسين تجربة المطوّر والدمج مع بقية الويب.

يمكنك العثور على القائمة الكاملة للاقتراحات والمراحل المتعلّقة بها في المستودع الرسمي أو تتبُّع حالة تنفيذها في المحرّكات على صفحة خارطة الطريق الرسمية للميزات.

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

اختيار الميزات وتجميعها

لنطّلِع على هذه الخطوات من خلال اختيار مجموعة ميزات عشوائية كمثال. لنفترض أنّني حدّدت أنّني أريد استخدام SIMD وخيوط المعالجة ومعالجة الاستثناءات في مكتبتي لأسباب تتعلّق بالحجم والأداء. في ما يلي متصفحات الأجهزة المتوافقة:

جدول يعرض توافق المتصفّح مع الميزات المحدّدة
يمكنك الاطّلاع على جدول الميزات هذا على webassembly.org/roadmap.

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

  • المتصفّحات المستندة إلى Chrome: تتوفّر ميزة "المعالجة المتعدّدة للخيوط" وSIMD ومعالجة الاستثناءات.
  • Firefox: تتوفّر تقنية SIMD وThread، ولكن لا تتوفّر معالجة الاستثناءات.
  • Safari: تتوفّر سلاسل المحادثات وشريحة SIMD ومعالجة الاستثناءات.
  • المتصفّحات الأخرى: نفترض أنّها تتوافق فقط مع WebAssembly الأساسي.

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

التحويل إلى مجموعات خصائص مختلفة

لا تشتمل WebAssembly على طريقة مدمجة لاكتشاف الميزات المتوافقة في وقت التشغيل، وبالتالي يجب أن يتم توفير جميع التعليمات في الوحدة على الهدف. لهذا السبب، عليك تجميع رمز المصدر في Wasm بشكل منفصل لكل مجموعة من مجموعات الميزات المختلفة هذه.

تختلف كل سلسلة أدوات ونظام إنشاء، وعليك الرجوع إلى مستندات المُجمِّع الخاص بك لمعرفة كيفية تعديل هذه الميزات. لتبسيط الأمر، سأستخدم مكتبة C++ أحادية الملف في المثال التالي وأوضح كيفية تجميعها باستخدام Emscripten.

سأستخدم SIMD من خلال محاكاة SSE2، وسأستخدم مؤشرات الترابط من خلال دعم مكتبة Pthreads، وسأختار بين معالجة استثناءات Wasm وتنفيذ JavaScript الاحتياطي:

# First bundle: threads + SIMD + Wasm exceptions
$ emcc main.cpp -o main.threads-simd-exceptions.mjs -pthread -msimd128 -msse2 -fwasm-exceptions
# Second bundle: threads + SIMD + JS exceptions fallback
$ emcc main.cpp -o main.threads-simd.mjs -pthread -msimd128 -msse2 -fexceptions
# Third bundle: threads + JS exception fallback
$ emcc main.cpp -o main.threads.mjs -pthread -fexceptions
# Fourth bundle: basic Wasm with JS exceptions fallback
$ emcc main.cpp -o main.basic.mjs -fexceptions

يمكن لرمز C++ نفسه استخدام #ifdef __EMSCRIPTEN_PTHREADS__ و#ifdef __SSE2__ للاختيار بشكل مشروط بين عمليات التنفيذ المتوازي (العمليات المتعدّدة والمخطّط المتسلسل للتعليمات) للوظائف نفسها وعمليات التنفيذ التسلسلي في وقت الترجمة. سيظهر الإجراء على النحو التالي:

void process_data(std::vector<int>& some_input) {
#ifdef __EMSCRIPTEN_PTHREADS__
#ifdef __SSE2__
  // …implementation using threads and SIMD for max speed
#else
  // …implementation using threads but not SIMD
#endif
#else
  // …fallback implementation for browsers without those features
#endif
}

لا يحتاج التعامل مع الاستثناءات إلى توجيهات #ifdef، لأنّه يمكن استخدامه بالطريقة نفسها من C++ بغض النظر عن التنفيذ الأساسي الذي تم اختياره من خلال علامات الترجمة.

تحميل الحِزمة الصحيحة

بعد إنشاء حِزم لجميع المجموعات النموذجية للميزات، عليك تحميل الحزمة الصحيحة من تطبيق JavaScript الرئيسي. لإجراء ذلك، عليك أولاً رصد الميزات المتاحة في المتصفّح الحالي. ويمكنك إجراء ذلك باستخدام مكتبة wasm-feature-detect. ومن خلال دمجها مع الاستيراد الديناميكي، يمكنك تحميل الحِزمة الأكثر تحسينًا في أي متصفّح:

import { simd, threads, exceptions } from 'https://unpkg.com/wasm-feature-detect?module';

let initModule;
if (await threads()) {
  if (await simd()) {
    if (await exceptions()) {
      initModule = import('./main.threads-simd-exceptions.mjs');
    } else {
      initModule = import('./main.threads-simd.mjs');
    }
  } else {
    initModule = import('./main.threads.mjs');
  }
} else {
  initModule = import('./main.basic.mjs');
}

const Module = await initModule();
// now you can use `Module` Emscripten object like you normally would

كلمات أخيرة

في هذا المنشور، أوضحت كيفية اختيار الحِزم وإنشاءها والتبديل بينها لمجموعات ميزات مختلفة.

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

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