वेब इंजीनियरिंग

आधुनिक वेब ऐप्स में WebAssembly (WASM): एक गाइड

C/C++ और Rust से कंपाइल की गई बाइनरी ब्राउज़र के अंदर लगभग नेटिव परफ़ॉर्मेंस कैसे देती हैं।

WebAssembly असल में क्या है

WebAssembly एक बाइनरी इंस्ट्रक्शन फ़ॉर्मैट है और एक कंपाइलेशन टारगेट भी। इसे हाथ से शायद ही कोई लिखता है। कंपाइलर C, C++, Rust, Zig या Go का कोड लेकर एक मॉड्यूल बनाता है, और ब्राउज़र उस मॉड्यूल को वैलिडेट करके मशीन कोड में कंपाइल करता है। यह फ़ॉर्मैट जान-बूझकर छोटा और जान-बूझकर नीरस रखा गया है: एक स्टैक मशीन, मनचाहे जंप की जगह स्ट्रक्चर्ड कंट्रोल फ़्लो, और इतना सख़्त टाइप सिस्टम कि वैलिडेशन बाइट्स पर एक ही लीनियर पास में पूरा हो जाता है। इन्हीं पाबंदियों की वजह से ब्राउज़र किसी मॉड्यूल को डाउनलोड होते-होते ही कंपाइल करना शुरू कर सकता है।

पर यह JavaScript का विकल्प नहीं है। मॉड्यूल के पास न DOM का एक्सेस होता है, न नेटवर्क का, न सिस्टम कॉल का। वह गणित कर सकता है, अपनी मेमोरी में पढ़-लिख सकता है, और उन फ़ंक्शन को कॉल कर सकता है जिन्हें होस्ट साफ़ तौर पर उसमें इंपोर्ट करता है। बाकी सब JavaScript का ही काम रहता है। व्यवहार में आप दोनों चीज़ें शिप करते हैं: एक मॉड्यूल जो गणना करता है, और JavaScript की एक पतली परत जो उसे बाइट्स देती है और नतीजे वापस बाहर लाती है।

कोडेक का काम WebAssembly में ही क्यों होना चाहिए

आम तौर पर दलील स्पीड की दी जाती है, और इस तरह बात रखने से इससे बेहतर दलील छिप जाती है। आधुनिक JavaScript इंजन तेज़ हैं, कभी-कभी हैरान करने की हद तक, लेकिन यह रफ़्तार वे अनुमान लगाकर हासिल करते हैं। JIT देखता है कि किसी फ़ंक्शन से कौन-से टाइप गुज़रते हैं, उसका एक खास वर्ज़न कंपाइल करता है और उस पर गार्ड लगा देता है। उसे कोई ऐसी वैल्यू दे दीजिए जिसकी उसे उम्मीद नहीं थी, तो गार्ड फ़ेल हो जाता है, कोड डीऑप्टिमाइज़ हो जाता है, और जब तक वह संभल नहीं जाता, एक्ज़ीक्यूशन धीमा चलता है। ऐप्लिकेशन कोड में यह दिखता तक नहीं। डिकोडर के इनर लूप में यही किसी फ़्रेम के बीचों-बीच अटकाव बन जाता है।

WebAssembly मॉड्यूल अपने टाइप बाइनरी में ही साथ लेकर चलता है। कुछ भी चलने से पहले कंपाइलर को हर वैल्यू का आकार पता होता है, इसलिए न कुछ अनुमान लगाना पड़ता है और न बाद में कुछ रद्द करना पड़ता है। लीनियर मेमोरी पर गार्बेज कलेक्शन भी नहीं होता, इसलिए हॉट पाथ में कलेक्शन के ठहराव का हिसाब नहीं रखना पड़ता। पीक थ्रूपुट में दोनों अक्सर उम्मीद से ज़्यादा करीब होते हैं। असली फ़र्क एकसारता में दिखता है, और यूज़र को एकसारता ही महसूस होती है, जब प्रोग्रेस बार झटके खाने की बजाय बराबर रफ़्तार से आगे बढ़ता है। फ़िक्स्ड-विड्थ 128-बिट SIMD इंस्ट्रक्शन भी यहाँ मदद करते हैं, क्योंकि पिक्सेल पर चलने वाले लूप और ऑडियो सैंपल पर चलने वाले लूप ठीक उसी तरह के होते हैं जो अच्छी तरह वेक्टराइज़ होते हैं।

फिर एक वजह ऐसी भी है जिसका परफ़ॉर्मेंस से कोई लेना-देना नहीं। इमेज और वीडियो कोडेक, PDF रेंडरर, OCR इंजन और स्पीच मॉडल पहले से ही परिपक्व C और C++ लाइब्रेरी के रूप में मौजूद हैं, जो बरसों की बग रिपोर्ट और गड़बड़ इनपुट वाले एज केस झेल चुकी हैं। libjpeg या Tesseract को JavaScript में दोबारा लिखना वीकेंड का प्रोजेक्ट नहीं है, और लंबे समय तक नतीजा मूल से खराब ही रहेगा। WebAssembly से आप मौजूदा चीज़ को कंपाइल करके शिप कर सकते हैं। प्रोडक्शन में ज़्यादातर WebAssembly इसी वजह से है, और यह किसी भी बेंचमार्क से ज़्यादा ठोस वजह है।

लीनियर मेमोरी, और टैब की एक सीमा क्यों होती है

मॉड्यूल की मेमोरी एक ही लगातार बफ़र होती है। मॉड्यूल के अंदर के पॉइंटर इसी बफ़र में ऑफ़सेट होते हैं, बफ़र 64 KiB के पेज में बढ़ता है, और wasm32 में ये पॉइंटर 32 बिट के होते हैं, जिससे एड्रेस की जा सकने वाली मेमोरी 4 GiB पर सीमित हो जाती है। ब्राउज़र इससे काफ़ी पहले रुक जाते हैं, और फ़ोन उससे भी बहुत पहले। इससे तीन नतीजे निकलते हैं, और प्रोडक्शन में परेशानी इन्हीं से होती है।

मेमोरी असल में सिर्फ़ बढ़ती है, घटती नहीं। memory.grow बफ़र को बड़ा करता है, लेकिन जब मॉड्यूल का एलोकेटर कोई ब्लॉक खाली करता है, तो वह ब्लॉक ब्राउज़र को नहीं, मॉड्यूल के अपने हीप में वापस जाता है। इसलिए एक बड़ा काम टैब की मेमोरी का सबसे ऊँचा स्तर बढ़ा देता है, और यह तब तक बना रहता है जब तक वह इंस्टेंस ज़िंदा है। अगर आप एक के बाद एक बड़ी फ़ाइलें प्रोसेस करते हैं, तो हर काम के बीच इंस्टेंस हटाकर नया बनाना इस भरोसे से ज़्यादा भरोसेमंद है कि free कुछ वापस लौटाएगा।

बढ़ने के लिए लगातार जगह का रिज़र्वेशन भी चाहिए। मशीन में भरपूर RAM खाली होने पर भी grow फ़ेल हो सकता है, सिर्फ़ इसलिए कि एड्रेस स्पेस टुकड़ों में बँटा है। यह सबसे ज़्यादा 32-बिट बिल्ड और कम मेमोरी वाले फ़ोन पर दिखता है, और यह शुरुआत में किसी विनम्र इनकार के रूप में नहीं, बल्कि ऑपरेशन के बीच में एक एक्सेप्शन के रूप में सामने आता है।

आखिर में, डेटा को सीमा के आर-पार कॉपी करना पड़ता है। JavaScript किसी मॉड्यूल को File ऑब्जेक्ट नहीं दे सकता। आप फ़ाइल को ArrayBuffer में पढ़ते हैं, उन बाइट्स को लीनियर मेमोरी में कॉपी करते हैं, ऑपरेशन चलाते हैं, और फिर Blob बनाने के लिए नतीजे को वापस बाहर कॉपी करते हैं। कुछ पल के लिए इनपुट आपके पास दो बार होता है, और कोडेक को खुद जितनी वर्किंग मेमोरी चाहिए, वह अलग। फ़ाइल के साइज़ का एक गुना नहीं, कई गुना बजट रखें, इंटरफ़ेस में साइज़ की साफ़ सीमा तय करें, और जहाँ फ़ॉर्मैट इजाज़त दे वहाँ स्ट्रीमिंग करें। आपका लिखा हुआ इनकार उस क्रैश से बेहतर है जो आपने नहीं लिखा।

वह बाइनरी जो आपको शिप करनी पड़ती है

किसी गंभीर कोडेक का WebAssembly बिल्ड मेगाबाइट में नापा जाता है, और यह JavaScript की तरह कंप्रेस नहीं होता। मिनिफ़ाई किया हुआ JavaScript दोहराव वाला टेक्स्ट होता है, और gzip को ऐसा टेक्स्ट बहुत भाता है। कंपाइल किया हुआ मॉड्यूल पहले से ही कसा हुआ बाइनरी होता है, इसलिए कंप्रेशन का अनुपात खराब रहता है, हालाँकि Brotli कुछ मदद करता है। मशीन लर्निंग मॉडल में मामला और कठिन है: वेट्स के आगे रनटाइम का साइज़ न के बराबर है। इससे जो नियम निकलता है वह सीधा है, फिर भी लोग उसे तोड़ते हैं। पेज लोड होते समय मॉड्यूल लोड न करें। उसे तब लोड करें जब यूज़र ऑपरेशन करने का फ़ैसला कर ले।

कुछ तरीके इस बाद में होने वाले लोड की तकलीफ़ कम करते हैं। फ़ाइल को application/wasm के रूप में सर्व करें, ताकि WebAssembly.instantiateStreaming पूरा डाउनलोड पहले बफ़र करने की बजाय बाइट्स आते-आते कंपाइल कर सके, क्योंकि गलत MIME टाइप चुपचाप आपसे यह फ़ायदा छीन लेता है। फ़ाइल को कंटेंट-हैश वाला URL और immutable कैश हेडर दें, ताकि दोबारा आने पर कुछ भी खर्च न हो। बड़े मॉड्यूल के लिए ब्राउज़र कंपाइल किया हुआ कोड भी कैश करते हैं, यानी लौटकर आने वाला विज़िटर डाउनलोड के साथ-साथ कंपाइलेशन से भी बच सकता है।

यह साइट इसी सोच पर बनी है। पेज के शुरुआती लोड में WebAssembly जैसा कुछ भी नहीं है। OCR टूल अपना इंजन तभी जोड़ता है जब आप सच में OCR चलाते हैं, और ट्रांसक्रिप्शन वर्कर पहली बार ट्रांसक्राइब करने पर अपना स्पीच मॉडल डाउनलोड करता है, फिर ब्राउज़र कैश के भरोसे चलता है। जो व्यक्ति JPG को PNG में कन्वर्ट करके चला जाता है, वह इनमें से कुछ भी डाउनलोड नहीं करता, क्योंकि वह रास्ता WebAssembly को छूता ही नहीं। वह बस canvas पर एक ड्रॉ और एक एन्कोड कॉल है, और दोनों काम ब्राउज़र का अपना नेटिव कोड करता है।

जहाँ WebAssembly गलत जवाब है

इसके पास DOM का एक्सेस नहीं है, इसलिए DOM का हर ऑपरेशन ग्लू कोड के ज़रिए वापस JavaScript में जाता है। WebAssembly में कंपाइल किया गया यूज़र इंटरफ़ेस फ़्रेमवर्क हर इंटरैक्शन पर यह टोल चुकाता है। यह आवाजाही सस्ती है, पर मुफ़्त नहीं, और यह लगातार होती रहती है।

इसी वजह से स्ट्रिंग के साथ काम करना भी अटपटा है। कोई साझा स्ट्रिंग टाइप नहीं है, इसलिए टेक्स्ट को सीमा पर एन्कोड, कॉपी और डिकोड करना पड़ता है। जिस काम में ज़्यादातर स्ट्रिंग की उलट-पलट हो, वह गणना से ज़्यादा समय डेटा को इधर से उधर पहुँचाने में लगा सकता है।

छोटे काम शायद ही कभी यह निवेश वसूल कर पाते हैं। डाउनलोड, कंपाइल और इंस्टैंशिएट, तीनों में असली समय लगता है। अगर गणना खुद कुछ मिलीसेकंड की है, तो मॉड्यूल वह समय कभी वापस नहीं निकाल पाता।

और जब प्लेटफ़ॉर्म पहले से ही नेटिव कोड में वह काम करता है, तो अपनी अलग कॉपी शिप करना उल्टा कदम है। WebCodecs वीडियो को एन्कोड और डिकोड करता है। Web Audio ऑडियो ग्राफ़ संभालता है। Canvas इमेज को डिकोड करता है, रीसाइज़ करता है और फिर से एन्कोड करता है। जो काम drawImage पहले से करता है, उसके लिए एक रीसैंपलर को एक मेगाबाइट WebAssembly में लपेटना इस गलती का सबसे साफ़ उदाहरण है।

एक बात और है, जो सीमा कम और जाल ज़्यादा है। WebAssembly कॉल सिंक्रोनस होते हैं। देर तक चलने वाला कॉल जिस थ्रेड पर होता है उसे रोक देता है, और मेन थ्रेड पर इसका मतलब है जमा हुआ पेज, ठीक वैसे ही जैसे JavaScript का कोई लंबा लूप करता। भारी मॉड्यूल Worker में होने चाहिए। यह वैकल्पिक सलाह नहीं है।

पहले इन सवालों के जवाब ढूँढें

  • क्या कोई परिपक्व नेटिव लाइब्रेरी पहले से यह समस्या सही ढंग से हल करती है? उसे कंपाइल करने की यह सबसे मज़बूत वजह है, और JavaScript में दोबारा लिखने की सबसे कमज़ोर।
  • क्या काम संख्याओं का है, अपने आप में पूरा है, और इतना लंबा है कि डाउनलोड और कंपाइल की लागत निकाल सके? टाइप्ड बफ़र पर चलने वाले कसे हुए लूप ही बाज़ी मारते हैं।
  • क्या कोई ब्राउज़र API पहले से यह काम करता है? अगर WebCodecs, Web Audio या canvas से काम चल जाता है, तो उन्हीं का इस्तेमाल करें और कुछ भी शिप न करें।
  • क्या बाइनरी तब तक रुक सकती है जब तक यूज़र ऑपरेशन न माँगे, और क्या आप उस मेमोरी के साथ काम चला सकते हैं जो इंस्टेंस टैब की बाकी उम्र तक पकड़े रहेगा?
  • क्या यह Worker में चल सकता है? अगर इसे मेन थ्रेड पर ही चलना है और यह एक फ़्रेम से ज़्यादा समय लेता है, तो हल इसे वहाँ से हटाना है, ऑप्टिमाइज़ करना नहीं।

जैसे ही आप यह उम्मीद छोड़ देते हैं कि यह JavaScript की जगह ले लेगा, यह टेक्नोलॉजी बिल्कुल साधारण लगने लगती है। यह मौजूदा नेटिव कोड को ऐसे सैंडबॉक्स में चलाने का तरीका है जो कभी इसके लिए बना ही नहीं था, एक ऐसे मेमोरी मॉडल के साथ जिसका ध्यान आपको रखना है और एक ऐसे डाउनलोड के साथ जिसकी कीमत आपको चुकानी है। पहले दिन से इन दोनों लागतों को डिज़ाइन की शर्तें मानें, बाकी सब आम इंजीनियरिंग है।