استبدال مسار سريع في رمز JavaScript لتطبيقك باستخدام WebAssembly

سرعة فائقة بشكلٍ منتظم

في مقالاتنا السابقة، تحدثنا عن كيفية استخدام WebAssembly لإدخال منظومة مكتبة C/C++ المتكاملة إلى الويب. أحد التطبيقات التي تستخدم مكتبات C/C++ بشكل كبير هو squoosh، وهو تطبيق الويب الذي يتيح لك ضغط الصور باستخدام مجموعة متنوعة من برامج الترميز التي تم تجميعها من C++ إلى WebAssembly.

‫WebAssembly هو جهاز افتراضي منخفض المستوى يشغّل الرمز الثانوي المخزّن في ملفات .wasm. هذا الرمز الثنائي مصنّف بشكلٍ دقيق ومُنظَّم بطريقة تسمح بتحويله إلى رمز آلي وتحسينه للنظام المضيف بشكلٍ أسرع بكثير مما يمكن أن توفّره JavaScript. توفّر WebAssembly بيئة لتشغيل الرموز البرمجية التي تم وضع ميزة وضع الحماية في مساحة معيّنة والاقتران في الاعتبار منذ البداية.

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

المسار الرائج

في أداة squoosh، كتبنا دالة JavaScript تؤدي إلى تدوير ذاكرة التخزين المؤقت للصورة بمضاعفات 90 درجة. على الرغم من أنّ OffscreenCanvas سيكون مثاليًا للقيام بهذه المهمة، إلا أنّه غير متوافق مع جميع المتصفّحات التي كنا نستهدفها، ويتضمّن بعض الأخطاء في Chrome.

تكرّر هذه الدالة كل بكسل من صورة الإدخال وتنسخه إلى موضع مختلف في صورة الإخراج لتحقيق الدوران. بالنسبة إلى صورة بحجم 4094 بكسل x 4096 بكسل (16 ميغا بكسل)، ستحتاج إلى أكثر من 16 مليون تكرار لمحاولة الرمز البرمجي الداخلي، وهو ما نُطلق عليه "مسار المعالجة المُهمّ". وعلى الرغم من هذا العدد الكبير من التكرارات، يُنهي اثنان من كل ثلاثة متصفحات اختبرناها المهمة في ثانيتين أو أقل. مدة مقبولة لهذا النوع من التفاعل

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

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

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

WebAssembly لتحقيق أداء يمكن توقُّعه

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

كتابة رموز WebAssembly

في السابق، استخدمنا مكتبات C/C++ وجمعناها في WebAssembly لاستخدام وظائفها على الويب. لم نغيّر في رمز المكتبات، بل كتبنا ببساطة كميات صغيرة من رمز C/C++ لإنشاء جسر بين المتصفّح والمكتبة. هذه المرة، دافعنا مختلف: نريد كتابة شيء من الصفر مع وضع WebAssembly في الاعتبار لنتمكّن من الاستفادة من المزايا التي يقدّمها WebAssembly.

بنية WebAssembly

عند كتابة رمز للغة WebAssembly، من المفيد فهم المزيد عن ماهية WebAssembly.

لاقتباس WebAssembly.org:

عند تجميع جزء من رمز C أو Rust إلى WebAssembly، ستحصل على ملف .wasm يحتوي على تعريف وحدة. يتألّف هذا البيان من قائمة بملفّات برمجية "مستورَدة" تتوقّعها الوحدة من بيئتها، وقائمة بالملفّات البرمجية التي تتيحها هذه الوحدة للمضيف (الدوالّ والثوابت وشرائح الذاكرة) وبالتأكيد والتعليمات الثنائية الفعلية للدوالّ المضمّنة فيها.

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

في هذه الحالة، سنحتاج إلى استخدام بعض الذاكرة الإضافية للسماح بالوصول بشكل عشوائي إلى وحدات بكسل الصورة وإنشاء نسخة مُحوَّلة من تلك الصورة. وهذا هو الغرض من WebAssembly.Memory.

إدارة الذاكرة

بعد استخدام ذاكرة إضافية، ستحتاج عادةً إلى تدبير هذه الذاكرة بطريقة ما. ما هي أجزاء الذاكرة المستخدَمة؟ ما هي الأدوات المجانية؟ في C، على سبيل المثال، لديك الدالة malloc(n) التي تعثر على مساحة ذاكرة تتألف من n بايت متتاليين. وتُعرف الدوالّ من هذا النوع أيضًا باسم "الموزّعين". بالطبع، يجب تضمين عملية تنفيذ أداة التوزيع المستخدَمة في وحدة WebAssembly، وسيؤدي ذلك إلى زيادة حجم ملفك. يمكن أن يختلف حجم وأداء وظائف إدارة الذاكرة هذه بشكل كبير حسب الخوارزمية المستخدَمة، ولهذا السبب توفّر العديد من اللغات عمليات تنفيذ متعددة للاختيار من بينها ("dmalloc" و"emmalloc" و"wee_alloc" وما إلى ذلك).

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

خيارات متعدّدة

إذا اطّلعت على وظيفة JavaScript الأصلية التي نريد تحويلها إلى WebAssembly، يمكنك ملاحظة أنّها رمز حسابي بحت بدون واجهات برمجة تطبيقات خاصة بـ JavaScript. وبالتالي، من المفترض أن يكون من السهل نقل هذا الرمز إلى أي لغة. قمنا بتقييم 3 لغات مختلفة يتم تجميعها مع WebAssembly، وهي C/C++ وRust وAssemblyScript. والسؤال الوحيد الذي يجب الإجابة عنه في كل لغة هو: كيف يمكننا الوصول إلى الذاكرة الأولية بدون استخدام وظائف إدارة الذاكرة؟

C وEmscripten

Emscripten هو مُجمِّع C لاستهداف WebAssembly. يهدف Emscripten إلى العمل كبديل فوري لمجمّعات C المعروفة، مثل GCC أو clang، وهو متوافق مع معظم العلامات. يشكّل ذلك جزءًا أساسيًا من مهمة Emscripten، ويهدف إلى تسهيل تحويل رمز C وC++ الحالي إلى WebAssembly.

إنّ الوصول إلى الذاكرة الأوّلية هو من طبيعة لغة C، وتُستخدَم المؤشرات لهذا السبب تحديدًا:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

هنا، نحوّل الرقم 0x124 إلى مؤشر إلى عدد صحيح (أو وحدات بايت) بدون إشارة وحجم 8 بت. يؤدي ذلك إلى تحويل المتغيّر ptr إلى صفيف يبدأ من عنوان الذاكرة 0x124، ويمكننا استخدامه مثل أي صفيف آخر، ما يتيح لنا الوصول إلى وحدات بايت فردية للقراءة والكتابة. في حالتنا، نحن ننظر إلى مخزن RGBA لصورة نريد إعادة ترتيبها لتحقيق التدوير. لنقل بكسل، علينا نقل 4 بايت متتالية في آنٍ واحد (بايت واحد لكل قناة: R وG وB وA). لتسهيل ذلك، يمكننا إنشاء صفيف من الأعداد الصحيحة غير الموقعة 32 بت. حسب الاصطلاح، ستبدأ صورة المدخلات لدينا من العنوان 4 وستبدأ صورة الإخراج مباشرة بعد انتهاء صورة الإدخال:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

بعد نقل دالة JavaScript بأكملها إلى C، يمكننا تجميع الملف C باستخدام emcc:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

وكالعادة، ينشئ emscripten ملف رمز غراء باسم c.js ووحدة Wasm باسم c.wasm. تجدر الإشارة إلى أن وحدة Wasm يتم ضغطها بتنسيق gzip فقط لحوالي 260 بايت فقط، بينما يبلغ حجم رمز الغراء حوالي 3.5 كيلوبايت بعد gzip. وبعد مرور بعض الوقت، تمكّنا من إزالة الرمز الملتصق وإنشاء مثيل لوحدات WebAssembly باستخدام واجهات برمجة التطبيقات vanilla API. غالبًا ما يكون ذلك ممكنًا مع Emscripten طالما أنك لا تستخدم أي شيء من مكتبة C standard.

Rust

Rust هي لغة برمجة جديدة وعصرية تتضمّن نظام أنواع غنيًا، ولا تتضمّن وقت تشغيل ونموذج ملكية يضمن أمان الذاكرة وأمان الخيط. توفّر لغة Rust أيضًا WebAssembly كميزة أساسية، وقد ساهم فريق Rust في توفير الكثير من الأدوات الممتازة للمنظومة المتكاملة لـ WebAssembly.

إحدى هذه الأدوات هي wasm-pack، التي أنشأها مجموعة عملrustwasm. wasm-pack يأخذ هذا الإطار البرمجي الرمز البرمجي الخاص بك ويحوّله إلى وحدة متوافقة مع الويب تعمل بشكلٍ تلقائي مع حِزم مثل webpack. توفّر "wasm-pack" تجربة ملائمة للغاية، ولكنها حاليًا متوافقة مع Rust فقط. تدرس المجموعة إضافة دعم للغات أخرى تستهدف WebAssembly.

في Rust، تكون الشرائح هي ما تكونه الصفائف في C. تمامًا كما هو الحال في C، علينا إنشاء شرائح تستخدِم عناوين البدء. يخالف ذلك نموذج أمان الذاكرة الذي يفرضّه Rust، لذا لاستخدام هذه الميزة، علينا استخدام الكلمة الرئيسية unsafe، التي تسمح لنا بكتابة رمز لا يمتثل لهذا النموذج.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

تجميع ملفات Rust باستخدام

$ wasm-pack build

ينتج عنها وحدة Wasm بحجم 7.6 كيلوبايت وبها حوالي 100 بايت من الرمز الملتصق (كلاهما بعد gzip).

AssemblyScript

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

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

بالنظر إلى مساحة العرض الصغيرة للنوع التي توفّرها دالة rotate()، كان من السهل نقل هذا الرمز إلى AssemblyScript. توفّر AssemblyScript الدالتَين load<T>(ptr: usize) وstore<T>(ptr: usize, value: T) لمحاولة الوصول إلى الذاكرة الأوّلية. لتجميع ملف AssemblyScript، نحتاج فقط إلى تثبيت حزمة npm AssemblyScript/assemblyscript وتشغيل

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

ستزوّدنا شركة AssemblyScript بوحدة Wasm بحجم 300 بايت تقريبًا وبدون رموز غراء. تعمل الوحدة فقط مع واجهات برمجة تطبيقات WebAssembly العادية.

تحليل WebAssembly الجنائي

إنّ حجم Rust الذي يبلغ 7.6 كيلوبايت كبير بشكلٍ مفاجئ مقارنةً باللغتَين الأخرتَين. هناك أداتان في منظومة WebAssembly المتكاملة يمكن أن تساعدك في تحليل ملفات WebAssembly (بصرف النظر عن اللغة المستخدَمة لإنشاء المحتوى)، وإخبارك بما يحدث، وتحسين حالتك.

Twiggy

Twiggy هي أداة أخرى من فريق WebAssembly في Rust لاستخراج مجموعة من البيانات الإحصائية من ملف WebAssembly. هذه الأداة ليست خاصة بلغة Rust، وتتيح لك فحص عناصر مثل مخطّط استدعاء الوحدة، وتحديد الأقسام غير المستخدَمة أو الزائدة، ومعرفة الأقسام التي تساهم في إجمالي حجم ملف وحدتك. يمكن تنفيذ الخطوة الأخيرة باستخدام الأمر top في Twiggy:

$ twiggy top rotate_bg.wasm
لقطة شاشة لتثبيت Twiggy

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

شريط Wasm

wasm-strip هي أداة من WebAssembly Binary Toolkit، أو wabt اختصارًا. يحتوي على بضع أدوات تتيح لك فحص وحدات WebAssembly والتعامل معها. wasm2wat هي أداة تفكيك يحوّل وحدة Wasm الثنائية إلى تنسيق يمكن للمستخدمين قراءته. يحتوي Wabt أيضًا على wat2wasm الذي يتيح لك تحويل هذا التنسيق السهل القراءة إلى وحدة wasm ثنائية. على الرغم من أنّنا استخدمنا كلتا الأداتَين المكمّلَين لفحص ملفات WebAssembly، تبيّن لنا أنّ wasm-strip هي الأداة الأكثر فائدة. wasm-strip تزيل الأقسام والبيانات الوصفية غير الضرورية من وحدة WebAssembly:

$ wasm-strip rotate_bg.wasm

يؤدي ذلك إلى تقليل حجم ملف وحدة Rust من 7.5 كيلوبايت إلى 6.6 كيلوبايت (بعد ضغط gzip).

استخدام Wasm

wasm-opt هي أداة من Binaryen. تأخذ هذه الأداة وحدة WebAssembly وتحاول تحسينها من حيث الحجم والأداء استنادًا إلى الرمز الثنائي فقط. تعمل بعض الأدوات، مثل Emscripten، على تنفيذ هذه الأداة، بينما لا تعمل بعض الأدوات الأخرى على تنفيذها. من الأفضل عادةً محاولة توفير بعض البايت الإضافية باستخدام هذه الأدوات.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

باستخدام wasm-opt، يمكننا تقليص عدد قليل من وحدات البايت لترك إجمالي 6.2 كيلوبايت بعد gzip.

#![no_std]

بعد إجراء بعض الاستشارات والأبحاث، أعدنا صياغة رمز Rust الخاص بنا بدون استخدام مكتبة Rust العادية، وذلك باستخدام ميزة #![no_std]. يؤدي ذلك أيضًا إلى إيقاف عمليات تخصيص الذاكرة الديناميكية تمامًا، ما يؤدي إلى إزالة رمز التحديد من وحدتنا. تجميع ملف Rust باستخدام

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

أدّى ذلك إلى إنشاء وحدة wasm بحجم 1.6 كيلوبايت بعد wasm-opt وwasm-strip وgzip. ومع أنّه لا يزال أكبر من الوحدات التي يتم إنشاؤها باستخدام C وAssemblyScript، إلا أنّه صغير بما يكفي ليُعتبر خفيفًا.

الأداء

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

كيفية قياس الأداء

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

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

مقارنة الأداء

مقارنة السرعة لكل لغة
مقارنة السرعة لكل متصفّح

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

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

الجهد

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

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

إنّ Rust مع wasm-pack ملائم للغاية أيضًا، ولكنه ينجح بشكلٍ أفضل في مشروعات WebAssembly الأكبر حجمًا التي تتطلّب ربطًا وإدارة ذاكرة. كان علينا الابتعاد قليلاً عن المسار الصحيح لتحقيق حجم ملف competitiv.

أنشأت C وEmscripten وحدة WebAssembly صغيرة جدًا وذات أداء عالٍ بشكل تلقائي، ولكن بدون الجرأة على الانتقال إلى رمز التجميع وتقليله إلى الحد الأدنى من الاحتياجات، ينتهي الأمر بأن يكون الحجم الإجمالي (وحدة WebAssembly + رمز التجميع) كبيرًا جدًا.

الخاتمة

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

الرسم البياني للمقارنة

بمقارنة حجم الوحدة / أدائها للغات المختلفة التي استخدمناها، يبدو أن الخيار الأفضل هو C أو AssemblyScript. قرّرنا طرح Rust. هناك عدة أسباب لهذا القرار: يتم تجميع كل برامج الترميز المضمّنة في Squoosh حتى الآن باستخدام Emscripten. أردنا توسيع نطاق معرفتنا بالمنظومة المتكاملة لـ WebAssembly واستخدام لغة مختلفة في مرحلة الإنتاج. تُعد AssemblyScript بديلاً قويًا، ولكن المشروع صغير نسبيًا وبرنامج التحويل البرمجي ليس ناضجًا مثل محول Rust.

على الرغم من أنّ الفرق في حجم الملف بين Rust وحجم اللغات الأخرى يبدو كبيرًا جدًا في الرسم البياني المبعثر، إلا أنّه ليس كبيرًا جدًا في الواقع: يستغرق تحميل 500 بايت أو 1.6 كيلوبايت حتى أكثر من 2 غيغابايت أقل من 1/10 من الثانية. ونأمل أن تُسدّد لغة Rust الفجوة من حيث حجم الوحدة قريبًا.

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

ويُرجى العِلم أنّ AssemblyScript كانت اكتشافًا عظيمًا. فهي تتيح لمطوّري البرامج على الويب إنشاء وحدات WebAssembly بدون الحاجة إلى تعلُّم لغة جديدة. كان فريق AssemblyScript سريع الاستجابة ويعمل بنشاط على تحسين مجموعة الأدوات. ومن المؤكد أننا سنراقب AssemblyScript في المستقبل.

تعديل: الصدأ

بعد نشر هذه المقالة، أشار إلينا فيتزجيرالد من فريق Rust إلى كتاب Rust Wasm الرائع الذي يحتوي على قسم حول تحسين حجم الملف. ومن خلال اتّباع التعليمات الواردة هناك (لا سيّما تفعيل تحسينات وقت الربط والمعالجة اليدوية للهلع) أتاح لنا كتابة رمز Rust "العادي" والرجوع إلى استخدام Cargo (npm من Rust) بدون المبالغة في حجم الملف. تنتهي وحدة Rust بحجم 370B بعد gzip. للاطّلاع على التفاصيل، يُرجى إلقاء نظرة على PR الذي فتحته على Squoosh.

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