मैंने एक प्रोडक्शन NextJS एप्लिकेशन को भारी Node.js standalone रनटाइम से एक हल्की आर्किटेक्चर में माइग्रेट करने का प्रयास किया। उद्देश्य था NextJS के output: 'export' का उपयोग करके स्टैटिक एसेट्स बनाना, जिन्हें उसी Docker इमेज में बंडल किए गए एक कस्टम, हल्के Go सर्वर द्वारा सर्व किया जा सके।
एप्लिकेशन एक जटिल, अंतरराष्ट्रीयकृत वेबसाइट थी जो आठ अलग-अलग लोकेल्स को सपोर्ट करती थी। प्राथमिक आवश्यकता एक यूनिवर्सल बिल्ड बनाए रखना था। इसका मतलब था कि Docker इमेज को environment variables से अज्ञेय (agnostic) होना चाहिए ताकि बिना रीबिल्ड किए अलग-अलग स्टेजेस में डिप्लॉय किया जा सके। मुख्य लक्ष्य भारी NextJS रनटाइम की RAM खपत से छुटकारा पाना था। NodeJS/Bun रनटाइम standalone मोड में ~ 200-450MB RAM और स्टैटिक एक्सपोर्ट के लिए ~140MB RAM खपत करता था।
यूनिवर्सल बिल्ड हासिल करने के लिए, बिल्ड प्रक्रिया से environment variables हटा दिए गए। स्टैटिक फ्रंटएंड में API endpoints को बेक करने के बजाय, /api/graphql जैसे स्टैटिक पाथ्स का उपयोग किया गया। इन रिक्वेस्ट्स को संभालने की जिम्मेदारी बंडल किए गए Go सर्वर को दी गई, जो एक प्रॉक्सी की तरह काम करता था और ट्रैफिक को रनटाइम एनवायरनमेंट वेरिएबल्स द्वारा परिभाषित उपयुक्त बैकएंड सर्विसेज तक फॉरवर्ड करता था।
इमेज ऑप्टिमाइज़ेशन को भी इसी तरह NextJS रनटाइम से अलग कर दिया गया। एक कस्टम इमेज लोडर लागू किया गया जो इमेज रिक्वेस्ट्स को एक समर्पित CDN पाथ पर री-राइट करता था, ताकि स्टैटिक एक्सपोर्ट इमेज प्रोसेसिंग लॉजिक से स्वतंत्र रहा।
डायनेमिक रूट्स, जैसे स्लग के माध्यम से एक्सेस किए जाने वाले ब्लॉग पोस्ट, को संभालने को लेकर एक बड़ी बाधा सामने आई। NextJS Static Exports में सभी पाथ्स को बिल्ड समय पर परिभाषित करने के लिए generateStaticParams आवश्यक होता है। हर संभावित ब्लॉग पोस्ट को प्री-रेंडर करने की अक्षम्यता से बचने के लिए, आर्किटेक्चर में बदलाव कर डायनेमिक रूट सेगमेंट्स की जगह क्वेरी पैरामीटर्स का उपयोग किया गया। इससे सभी कंटेंट रिक्वेस्ट्स के लिए एक ही, जेनरिक स्टैटिक HTML फ़ाइल सर्व करना संभव हुआ, जिसे क्लाइंट-साइड एप्लिकेशन बाद में विशिष्ट डेटा से भर देता था।
आखिरकार यह प्रोजेक्ट सर्च इंजन ऑप्टिमाइज़ेशन से जुड़े एक अनसुलझे टकराव के कारण छोड़ना पड़ा, खासकर Canonical URL मेटाडेटा की आवश्यकताओं के चलते।
अधिकांश मेटाडेटा बिना किसी बड़े प्रभाव के स्टैटिक रह सकता है, लेकिन canonical टैग के सही काम करने के लिए पूर्ण डोमेन सहित एक absolute URL आवश्यक होता है। NextJS की app डायरेक्टरी में मेटाडेटा को generateMetadata या स्टैटिक metadata एक्सपोर्ट के जरिए परिभाषित करना पड़ता है—दोनों ही स्टैटिक एक्सपोर्ट्स के लिए केवल बिल्ड समय पर चलते हैं। Node.js सर्वर के बिना इसे रनटाइम तक टालने का कोई बिल्ट-इन मैकेनिज़्म नहीं है। नतीजतन, एक यूनिवर्सल स्टैटिक एक्सपोर्ट में डोमेन नाम बिल्ड समय पर ज्ञात नहीं हो सकता।
इसे प्रस्तावित आर्किटेक्चर के भीतर ठीक करने के लिए, Go सर्वर को स्टैटिक HTML पार्स कर रनटाइम पर canonical टैग की स्ट्रिंग के भीतर डोमेन या डोमेन-जैसे placeholder मान को डायनेमिक रूप से बदलना पड़ता। इस तरीके को खारिज किया गया, क्योंकि यह वस्तुतः स्टैटिक एक्सपोर्ट पर Go-आधारित टेंपलेटिंग लागू करने के बराबर है। ऐसी छेड़छाड़ न तो सुरुचिपूर्ण है और न ही सुरक्षित—यह हाइड्रेशन समस्याओं का बड़ा जोखिम पैदा करती है, जहां क्लाइंट-साइड React एप्लिकेशन बदले हुए सर्वर-सेंट HTML से मेल नहीं खाता।
जब तक आपको SEO रैंकिंग की ज़रूरत नहीं है, मल्टी-एन्वायरनमेंट Docker इमेज के साथ हल्का रनटाइम बनाना संभव है। अन्यथा, यह केवल बहुत हैकी समाधानों से ही संभव है—जैसे कस्टम टेंपलेटिंग इंजन के जरिए स्टैटिक .html और .js के भीतर placeholder ENV वेरिएबल्स को बदलना।
वे NextJS ऐप्स जिनमें BACKEND_URL, API_ENDPOINT जैसे डायनेमिक environment variables या कस्टम री-राइट की ज़रूरत होती है, उनके लिए आप एक हल्का कस्टम Golang-आधारित सर्वर इस्तेमाल कर सकते हैं। यह आसान है और काम करता है (~13 MB RAM उपयोग)। कई फीचर्स के लिए ढेरों वर्कअराउंड लागू किए जा सकते हैं।
जिन वेबसाइट्स में SEO महत्वपूर्ण नहीं है, उनके लिए आपके स्टैटिक एक्सपोर्ट को होस्ट करने वाले बहुत से सर्वर मौजूद हैं। उदाहरण के लिए, अपने एक प्रोजेक्ट में मैं Rust-based static-web-server का उपयोग करता हूं (~8 MB RAM उपयोग)।
यह प्रयास आंकड़ों में ऐसा दिखा:
अगर मैं हर कीमत पर इच्छित नतीजा हासिल करता, तो इस अपडेट के लिए लगभग 4× अधिक जोड़ी गई पंक्तियाँ और ~100 फ़ाइलों में बदलाव की ज़रूरत पड़ती।
