

1/1/1970
मैंने 2 दिनों में SSG कैसे लागू किया
नमस्ते! एक साल पहले, मुझे लगता था कि यह असंभव है. लेकिन मैंने अभी Foony के लिए Static Site Generation (SSG) को 2 दिनों में लागू किया है, और मैं इसके बारे में काफी उत्साहित हूँ. यह Foony के लिए SSG को हल करने की मेरी पहली कोशिश भी नहीं है. मैंने पहले NextJS, Vike, Astro, Gatsby, और कुछ अन्य समाधानों को देखा है. मैंने NextJS के साथ एक झूठी शुरुआत भी की थी, लेकिन Foony के SPA की जटिलता और हजारों फाइलों के कारण मुश्किलों में फँस गया. वह माइग्रेशन एक दुःस्वप्न होता और महीनों लग जाते. साइट पर काम करने वाले हर व्यक्ति के लिए भी अतिरिक्त जटिलता जुड़ जाती क्योंकि उन्हें NextJS और उसकी विचित्रताएँ सीखनी पड़तीं.
मुझे कुछ हल्का और लागू करने में आसान चाहिए था. कुछ ऐसा जो हमें SSG के बारे में सोचे बिना उसी तरह कोड लिखते रहने दे जैसे हम लिखते आ रहे हैं (useMediaQuery को छोड़कर, उसका कोई असली रास्ता नहीं है). नीचे मैं बताऊँगा कि मैंने एक कस्टम समाधान क्यों चुना, मुझे किन विशिष्ट चुनौतियों का सामना करना पड़ा (खासकर React की Suspense boundaries के साथ), और मैंने उन्हें कैसे हल किया.
मानक समाधान क्यों नहीं?
जब मैंने पहली बार Foony में SSG जोड़ने पर विचार किया, तो मैंने स्वाभाविक रूप से NextJS (इंडस्ट्री स्टैंडर्ड), Vike, और Astro पर विचार किया.
NextJS: बहुत ज्यादा माइग्रेशन
NextJS शक्तिशाली है, लेकिन इसके लिए Foony के मौजूदा React SPA का बड़े पैमाने पर माइग्रेशन करना पड़ता. हमारे पास हजारों फाइलें, जटिल राउटिंग लॉजिक, और ढेर सारा कस्टम इंफ्रास्ट्रक्चर है. NextJS पर माइग्रेट करने का मतलब होता:
- हमारे पूरे राउटिंग सिस्टम को फिर से लिखना
- गेम्स और कंपोनेंट्स को लोड करने का तरीका पुनर्गठित करना
- फीचर पैरिटी पर वापस पहुँचने के लिए ही महीनों का काम
- यूजर्स के लिए संभावित ब्रेकिंग बदलाव
- इमेजेस को संभालने का तरीका बदलना
- काफी धीमा बिल्ड टाइम (संभवतः 5-30 मिनट. मेरे पास इसके लिए ठोस आँकड़े नहीं हैं, सिवाय इस GitHub पर 5 साल पुरानी चर्चा के)
- पूरी टीम को कुछ नया (NextJS) सीखना, और हमेशा के लिए धीमी डेवलपर वेलॉसिटी
- हर बार कोड को माइग्रेट करना जब NextJS ब्रेकिंग बदलाव करने का फैसला करे.
मैंने NextJS के साथ एक झूठी शुरुआत भी की, लेकिन जल्दी ही समझ गया कि माइग्रेशन की लागत बहुत ज्यादा थी. जटिलता इसके लायक नहीं थी.
Vike: समान जटिलता
Vike (पहले vite-plugin-ssr) में भी समान समस्याएँ थीं. हालाँकि यह NextJS से अधिक लचीला है, फिर भी हमारे कोडबेस के महत्वपूर्ण पुनर्गठन की आवश्यकता होती. लर्निंग कर्व और माइग्रेशन का प्रयास लाभों के लिए उचित नहीं था.
Astro: गलत आर्किटेक्चर
Astro कंटेंट-हैवी साइट्स के लिए शानदार है, लेकिन Foony एक जटिल मल्टीप्लेयर गेम प्लेटफॉर्म है. हमें रियल-टाइम अपडेट्स, WebSocket कनेक्शन, और डायनैमिक React कंपोनेंट्स की जरूरत है. Astro का आर्किटेक्चर बस वैसा नहीं है जैसा हम बना रहे हैं.
समाधान: कस्टम SSG
कुछ दिन पहले i18n के बाद लागू किए गए अपने "नकली SSG" दृष्टिकोण से प्रोत्साहित होकर, मैंने Foony के SSG के लिए एक छोटा, हल्का, कस्टम समाधान चुना.
मेरे "नकली SSG" दृष्टिकोण में ब्लॉग पोस्ट वाले पेजेस (
/postsरूट्स और गेम पेजेस) से ब्लॉग पोस्ट कंटेंट निकालना, और उन्हें ठीक वहीं रखना शामिल था जहाँ क्लाइंट उन्हें रेंडर करता, खासकर सर्च इंजन और LLMs के लिए ताकि वे Foony को समझने में मदद कर सकें. इसमें ld+json schema और कुछ छोटे SEO चीजें भी लागू की गई थीं.
दृष्टिकोण सरल है:
- मौजूदा React SPA पर बनाएँ: कोई माइग्रेशन की जरूरत नहीं, बस बिल्ड टाइम पर SSG जनरेशन जोड़ें.
renderToReadableStreamका उपयोग करें: React 18 का स्ट्रीमिंग SSR API Suspense को नेटिवली हैंडल करता है.- स्टैटिक HTML फाइलें जनरेट करें: बिल्ड टाइम पर रूट्स को प्री-रेंडर करें और उन्हें स्टैटिक फाइलों के रूप में सर्व करें, हमारे SitemapGenerator का उपयोग करके रूट्स की सूची प्राप्त करें.
- मौजूदा कोडबेस में न्यूनतम बदलाव: अधिकांश कंपोनेंट्स जैसे हैं वैसे काम करते हैं.
मूल कार्यान्वयन client/src/generators/GenerateShellSsgFromSitemap.ts में रहता है. यह एक sitemap पढ़ता है, React के renderToReadableStream का उपयोग करके प्रत्येक रूट को रेंडर करता है, और HTML को स्टैटिक फाइलों में लिखता है. सरल, बिल्कुल जैसा मुझे पसंद है!
यह काफी तेज भी निकला. लगभग 2,800 रूट्स 10 सेकंड में रेंडर हो गए. बढ़िया. यह NextJS, Gatsby, और Astro से काफी तेज है. <img alt="लगने वाला समय दिखाता SSG कंसोल लॉग" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.ssg_speed.webp" style={{ margin: "8px auto", height: 120, display: "block" }} />
मैं सरलता के बारे में बहुत कुछ कह सकता हूँ. भले ही "जटिलता की कमी" के कारण बड़ी कंपनियों में आपको प्रमोशन नहीं मिले, सरल कोड सुंदर, मेंटेनेबल होता है, और सामान्य रूप से डेवलपर वेलॉसिटी के लिए बहुत बेहतर है. यह कुछ ऐसा है जो मुझे Zen सिद्धांतों के बारे में वास्तव में पसंद है.
Suspense Boundary की समस्या
तो अब मेरे पास SSG था, और कंटेंट HTML में दिखाई दिया... लेकिन मेरे पेज खाली थे! कैसे?! <img alt="SSG खाली पेज" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blank_page.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />
पता चला कि renderToReadableStream में अभी भी Suspense boundaries हैं, भले ही आप await stream.allReady करें. मेरा अनुमान है कि ऐसा इसलिए है क्योंकि यह एक "stream" है, और बाइट्स प्राप्त होते ही क्लाइंट्स को पास करने के लिए डिज़ाइन किया गया है.
React क्या आउटपुट करता है
जब आप Suspense के साथ renderToReadableStream का उपयोग करते हैं, तो React इस तरह HTML आउटपुट करता है:
<!--$?-->
<template id="B:0"></template>
<!--/$-->
<div hidden id="S:0">
<!-- Actual content here -->
</div>
...
<script>/*Script that replaces the suspense boundaries*/</script>
<template id="B:0"> एक प्लेसहोल्डर है जहाँ कंटेंट जाना चाहिए. <div hidden id="S:0"> में वास्तविक रेंडर किया गया कंटेंट होता है. B:0 संख्या के अनुसार S:0 से मेल खाता है (0-आधारित इंडेक्स).
JavaScript के बिना, सर्च इंजन (तुम्हें देख रहा हूँ, Bing) और LLMs को केवल टेम्पलेट प्लेसहोल्डर के साथ लगभग खाली पेज दिखाई देगा. यह SSG के पूरे उद्देश्य को विफल कर देता है!
मुझे इन Suspense boundaries को हटाने का कोई साफ तरीका नहीं दिखा, इसलिए मेरा समाधान कुछ टेस्ट और एक resolveSuspenseBoundaries फंक्शन लिखना था ताकि इन्हें स्वैप किया जा सके. यह HTML को पार्स करने और JSDOM जैसी किसी चीज के साथ स्क्रिप्ट को एक्जीक्यूट करने से तेज था. और, अधिक महत्वपूर्ण बात, यह उस चीज की आवश्यकता थी जो मैंने योजना बनाई थी: सर्च इंजन / LLMs के लिए JavaScript के बिना एक अच्छी, पठनीय साइट, लेकिन क्लाइंट पर Suspense boundaries और हाइड्रेशन के समर्थन के साथ.
ट्रांसफॉर्मेशन का परीक्षण
मैंने DOM से कुछ उदाहरण लेकर ट्रांसफॉर्मेशन के लिए टेस्ट लिखकर शुरुआत की, जो मेरे पास था (JavaScript डिसेबल्ड), और जो मैं चाहता था (JavaScript एनेबल्ड). मैंने इन्हें एक LLM में फीड किया और इसे टेस्ट जनरेशन हैंडल करने दिया, जिसमें यह काफी अच्छा है.
ये टेस्ट client/src/generators/ssr/renderRoute.test.ts में रहते हैं और सुनिश्चित करते हैं कि ट्रांसफॉर्मेशन सही ढंग से काम करे. टेस्ट इन्हें कवर करते हैं:
- सरल boundary प्रतिस्थापन (ब्लॉग लिस्टिंग)
- टेम्पलेट और क्लोजिंग कमेंट के बीच कंटेंट के साथ जटिल boundaries
- एकाधिक boundaries
- कमेंट मार्कर के बिना boundaries
- एज केसेस
इस तरह का "TDD" इस उपयोग के मामले के लिए काफी उपयोगी है जहाँ आपके पास अपेक्षित इनपुट और आउटपुट हैं.
इसे "हर चीज में TDD करो क्योंकि Robert C. Martin ने कहा है" के साथ भ्रमित नहीं किया जाना चाहिए (जो आपकी टीम की डेवलपमेंट वेलॉसिटी को धीमा कर देगा). आपको UI या अपने कोड के उन क्षेत्रों के लिए TDD का उपयोग नहीं करना चाहिए जो लगातार बदल रहे हैं!
समाधान: resolveSuspenseBoundaries
अब जब टेस्ट तैयार थे, मैंने LLM से resolveSuspenseBoundaries के लिए फंक्शन लिखवाया. मैंने इसके लिए cheerio चुना ताकि RegEx की कमजोरी से बच सकूँ, भले ही यहाँ RegEx का उपयोग करने से SSG समय में लगभग 40% की कटौती होती.
export function resolveSuspenseBoundaries(html: string): {html: string; didResolveSuspense: boolean} {
const originalHtml = html;
const $ = cheerio.load(originalHtml, {xml: false, isDocument: false, sourceCodeLocationInfo: true});
const operations: Array<{index: number; removeLength: number; insertText?: string}> = [];
// Collect hidden divs with their content and positions.
const hiddenDivs = new Map<string, {content: string; divStartIndex: number; divEndIndex: number}>();
$('div[hidden][id^="S:"]').each((_, el) => {
const id = $(el).attr('id');
if (!id) {
return;
}
const boundaryId = id.substring(2);
const content = $(el).html() || '';
const {startOffset, endOffset} = el.sourceCodeLocation ?? {};
if (typeof startOffset === 'number' && typeof endOffset === 'number') {
hiddenDivs.set(boundaryId, {content, divStartIndex: startOffset, divEndIndex: endOffset});
}
});
if (hiddenDivs.size === 0) {
return {html: originalHtml, didResolveSuspense: false};
}
// Find templates (B:0) and replace them with the matching hidden content (S:0),
// following React’s internal $RV behavior.
$('template[id^="B:"]').each((_, el) => {
const id = $(el).attr('id');
if (!id) {
return;
}
const boundaryId = id.substring(2);
const divInfo = hiddenDivs.get(boundaryId);
if (!divInfo) {
return;
}
const {startOffset, endOffset} = el.sourceCodeLocation ?? {};
if (typeof startOffset !== 'number' || typeof endOffset !== 'number') {
return;
}
const templateIndex = startOffset;
const templateLength = endOffset - startOffset;
const afterTemplate = originalHtml.substring(templateIndex + templateLength);
const closingCommentMatch = afterTemplate.match(/<!--\/[amp;]-->/);
const removeEndIndex = closingCommentMatch
? templateIndex + templateLength + closingCommentMatch.index!
: templateIndex + templateLength;
const divContentStartIndex = originalHtml.indexOf('>', divInfo.divStartIndex) + 1;
const divContentEndIndex = originalHtml.lastIndexOf('</', divInfo.divEndIndex);
const divContent = originalHtml.substring(divContentStartIndex, divContentEndIndex);
operations.push({index: templateIndex, removeLength: removeEndIndex - templateIndex});
operations.push({index: templateIndex, removeLength: 0, insertText: divContent});
operations.push({index: divContentStartIndex, removeLength: divContentEndIndex - divContentStartIndex});
operations.push({index: divInfo.divStartIndex, removeLength: divContentStartIndex - divInfo.divStartIndex});
operations.push({index: divContentEndIndex, removeLength: divInfo.divEndIndex - divContentEndIndex});
});
operations.sort((a, b) => (a.index !== b.index ? b.index - a.index : b.removeLength - a.removeLength));
let resultHtml = originalHtml;
for (const operation of operations) {
resultHtml = resultHtml.slice(0, operation.index) + (operation.insertText ?? '') + resultHtml.slice(operation.index + operation.removeLength);
}
return {html: resultHtml, didResolveSuspense: true};
}
यह सुनिश्चित करता है कि लगभग खाली पेज देखने के बजाय, सर्च इंजन और LLMs पूरी तरह से रेंडर किया गया पेज देखें.
अब हमारे पास JavaScript के बिना अच्छी तरह से काम करने वाला SSG है!
<img alt="Foony के ब्लॉग के लिए No JavaScript SSG" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blog_ssg.webp" style={{ margin: "8px auto", height: 340, display: "block" }} />
लंबे समय में, संभव है कि React अपने Suspense फॉर्मेट को बदले. जब मेरे पास उन पेजों के लिए बेहतर समाधान होगा जो लेज़ी-लोडेड हैं (और इस प्रकार Suspense boundaries की आवश्यकता है) तो मैं Suspense रिज़ॉल्यूशन कोड हटा सकता हूँ.
हाइड्रेशन रणनीति (अपडेट: इसमें 3 दिन + 1 अतिरिक्त दिन लगा)
हाइड्रेशन चुनौतीपूर्ण है. मुझे यह पता था. लेकिन, थोड़े काम के बाद, मैं इसे काम करवाने में कामयाब रहा!
हाइड्रेशन में लगा कुल समय: 3 दिन, साथ ही dehydration दृष्टिकोण को बदलने के लिए 1 अतिरिक्त दिन.
सबसे मुश्किल हिस्सा बस वह पहला न्यूनतम, काम करने वाला hydrate प्राप्त करना था. एक बार जब मैं navbar के साथ "Hello World" रेंडर करने में कामयाब हुआ, तो मुझे यह विश्वास हो गया कि, हाँ, इसमें पूरा महीना नहीं लग सकता!
<img alt="Foony's Hello World navbar के साथ सफलतापूर्वक hydrate हो रहा है" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_mvp.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />
उस पहले न्यूनतम, काम करने वाले hydrate के लिए, मेरे सामने एक अनोखी चुनौती थी: मुझे हाइड्रेशन चाहिए था, लेकिन मुझे डेवलपर्स के Suspense boundaries के बारे में सोचे बिना सर्च इंजन और LLMs के लिए अच्छा SEO भी चाहिए था.
चुनौती
React हाइड्रेशन बेहद शाब्दिक है: यदि DOM उस पहले रेंडर के लिए वैसा नहीं दिखता जैसा React अपेक्षा करता है, तो आपको कंसोल में यह अच्छा, लगभग बेकार एरर मेसेज मिलता है, और React सब कुछ फेंक देता है और शुरू से फिर से रेंडर करता है. यहाँ तक कि यह बताने के लिए कि क्या गलत हुआ, कोई diff भी नहीं!
हमारे मामले में, SSG ने इसे कई तरीकों से और बदतर बना दिया:
- हमने React 18 स्ट्रीमिंग Suspense आर्टिफैक्ट्स को हटाने/हल करने के लिए HTML को पोस्ट-प्रोसेस किया (जो बॉट्स के लिए बढ़िया है).
- क्लाइंट के पास हमेशा वही डेटा समय (t = 0) पर उपलब्ध नहीं था जो सर्वर रेंडर के पास था (SSG डेटा, ब्लॉग मेटाडेटा, आदि).
- हमारा i18n डिफ़ॉल्ट रूप से "lazy" है, जिसका मतलब है कि पहले रेंडर के लिए अनुवाद गायब हो सकते हैं जब तक कि आप यह रिकॉर्ड नहीं करते कि SSG के लिए कौन से अनुवाद उपयोग किए गए और React के रेंडर करने से पहले उन्हें इंजेक्ट करते हैं.
क्या काम किया (प्रारंभिक दृष्टिकोण: Dehydration)
पहले, मैंने कुछ चतुर और प्यारा करने की कोशिश की: मैंने HTML की Suspense boundaries को हल करने के लिए उपयोग किए गए कमांड्स को रिकॉर्ड करने के लिए एक कमांड पैटर्न का उपयोग किया, और रिवर्स ट्रांसफॉर्मेशन कमांड्स लौटाए ताकि मैं HTML को वापस वैसा ही कर सकूँ जैसा React को हाइड्रेशन के लिए चाहिए.
मेरी आशा थी कि मैं इस कमांड पद्धति के साथ index.html में बहुत कम बाइट्स शिप कर सकूँगा. लेकिन, अधिकांश चतुर समाधानों की तरह, यह विफल हो गया क्योंकि ब्राउज़र HTML को सूक्ष्म तरीकों से संशोधित करते हैं, जैसे ; या / को हटाना या जोड़ना, जो प्रतिस्थापन इंडेक्स को बिगाड़ देता है.
तकनीकी रूप से आप शायद इन सूक्ष्म ब्राउज़र बदलावों का हिसाब रख सकते हैं, लेकिन मैं ऐसा कुछ नाजुक शिप करने वाला नहीं था.
Suspense-boundary ट्रांसफॉर्मेशन को React की स्ट्रीमिंग markup में वापस "रिवर्स" करने की कोशिश करने के बजाय, मैंने कुछ बहुत सरल किया:
मूल, अनरिज़ॉल्व्ड HTML को <script type="text"> में बंडल करें.
यह "dehydration" दृष्टिकोण काम करता था, लेकिन मैंने इसे एक बेहतर समाधान से बदलने में एक अतिरिक्त दिन बिताया.
बेहतर दृष्टिकोण: Critical Path Suspense Boundary Replacement
प्रारंभिक कार्यान्वयन के बाद, मुझे अभी भी Suspense boundaries के साथ कुछ समस्याएँ आ रही थीं. तभी मुझे एहसास हुआ कि एक स्वच्छ, बेहतर, सरल समाधान था. मैंने dehydration दृष्टिकोण को critical path Suspense boundary replacement से बदल दिया, जो:
- हाइड्रेशन से पहले critical path लोड करता है: SSR के दौरान प्रीलोड किए गए कंपोनेंट्स की पहचान की जाती है और
hydrateRoot कॉल होने से पहले क्लाइंट पर प्रीलोड किया जाता है
- मेंटेन करना सरल है: कोई React internals या AST पार्सिंग आवश्यक नहीं (dehydration दृष्टिकोण को HTML पार्स और रिस्टोर करना पड़ता था)
- कम बाइट्स शिप करता है: हम अब React से मूल SSR रिस्पॉन्स को स्क्रिप्ट टैग में बंडल नहीं करते
- संभावित flash को रोकता है: HTML को dehydrate/rehydrate करने की जरूरत नहीं, संभावित विज़ुअल flash को समाप्त करता है
कार्यान्वयन ट्रैक करता है कि SSR के दौरान कौन से लेज़ी कंपोनेंट्स प्रीलोड किए गए (SSRLazyComponentTracker के माध्यम से), उनके इम्पोर्ट पाथ्स को हाइड्रेशन डेटा में शामिल करता है, और हाइड्रेशन से पहले सिंक्रोनसली प्रीलोड करता है. critical path कंपोनेंट्स बिना Suspense boundaries के सीधे रेंडर होते हैं, बिल्कुल SSR आउटपुट के साथ मेल खाते हुए.
बाकी सब के लिए, हम पहले क्लाइंट रेंडर को SSR/SSG के रूप में काम करवाते हैं. इसका मतलब है समान इनपुट का उपयोग करना, और उन इनपुट को hydrateRoot से पहले सिंक्रोनसली उपलब्ध कराना. यह हमारे "ssg-data" के माध्यम से बंडलिंग द्वारा किया जाता है.
विशेष रूप से, समायोजन ये थे:
SSR इनपुट को एक टेक्स्ट स्क्रिप्ट में बंडल करें
- SSG के दौरान, हम Vite मॉड्यूल entrypoint से ठीक पहले
<script type="text/foony-ssg" id="foony-ssg-data">...</script> इंजेक्ट करते हैं.
- उस स्क्रिप्ट में होता है:
html: हल किया गया HTML जो हमने वास्तव में स्टैटिक फाइल में शिप किया
ssgData: SSR रैपर द्वारा उपयोग किया गया सीरियलाइज़्ड SSGData. मैं इसे Proxy या कुछ और में अपडेट करने की योजना बना रहा हूँ ताकि केवल एक्सेस किया गया डेटा शामिल हो.
translationData: SSR के दौरान हमने जिन अनुवाद की key-value blobs को छुआ
हाइड्रेशन से ठीक पहले उन इनपुट्स को इंजेक्ट करें
main.tsx में, हम सिंक्रोनसली:
#root.innerHTML को सीरियलाइज़्ड हल किए गए HTML पर सेट करते हैं (ताकि DOM ठीक वैसा हो जैसा हाइड्रेशन देखता है)
- ऐप को
SSGDataProvider में लपेटें ताकि कंपोनेंट्स के पास पहले रेंडर पर समान SSGData हो
अनुवाद मूल्यों को इंजेक्ट करके i18n को तत्काल बनाएँ
- हम SSR के दौरान एक्सेस किए गए वास्तविक अनुवाद ऑब्जेक्ट्स को रिकॉर्ड करते हैं और उन्हें SSG स्क्रिप्ट में शिप करते हैं.
- क्लाइंट पर, हम उन्हें एक समर्पित
LocaleQueryer.inject() मेथड के माध्यम से सीधे LocaleQueryer के कैश में इंजेक्ट करते हैं, ताकि अनुवाद तुरंत उपलब्ध हों.
और इसके साथ, पहले रेंडर के पास वही डेटा है जो SSR के पास था!
useIsSSRMode() हुक पहले से ही client/src/generators/ssr/isSSRMode.ts में लागू है:
export function useIsSSRMode(): boolean {
const [isSSRMode, setIsSSRMode] = React.useState(true);
React.useEffect(() => {
// After mount (hydration complete), switch to client mode
setIsSSRMode(false);
}, []);
return isSSRMode;
}
यह हुक SSR के दौरान और पहले क्लाइंट रेंडर (हाइड्रेशन) पर true लौटाता है, फिर माउंट के बाद false पर स्विच करता है. UserBanner, Navbar, और Dialog जैसे कंपोनेंट्स हाइड्रेशन मिसमैच को रोकने के लिए पहले से इसका उपयोग करते हैं.
- बेहतर diffs के लिए React को पैच करें
मैं उम्मीद कर रहा था कि मैं बस hydration-overlay का उपयोग कर सकूँगा. लेकिन यह सक्रिय रूप से मेंटेन नहीं है, केवल React 18 तक समर्थित है, और प्रोडक्शन-रेडी नहीं था. इसलिए मैंने एक LLM से प्रेरणा के लिए repo क्लोन करवाया, और फिर इसने कुछ ही मिनटों में एक न्यूनतम hydration overlay बनाया. मुझे कुछ फैंसी नहीं चाहिए था, बस कुछ ऐसा जो डेवलपमेंट के दौरान दिखाई दे ताकि मैं पता लगा सकूँ कि चीजें कहाँ गलत हुईं.
यह नया overlay बहुत बेसिक है, इसलिए diffs बिल्कुल परफेक्ट नहीं हैं. React कमेंट्स को हटा देता है, स्टाइल एट्रिब्यूट्स के बाद ; जोड़ता है, whitespace को संशोधित करता है, और कुछ अन्य छोटी चीजें करता है जिनका हमारा overlay हिसाब नहीं रखता (अभी तक). हमारे overlay में HTML कमेंट्स भी शामिल हैं जिन्हें React अपने हाइड्रेशन के लिए अनदेखा करता है.
<img alt="हमारा नया hydration overlay" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_overlay.webp" style={{ margin: "8px auto", height: 315, display: "block" }} />
लेकिन यह यह पता लगाने के लिए काफी अच्छा है कि क्या ठीक करने की जरूरत है.
<img alt="React हाइड्रेशन के लिए हमारे SSG बनाम क्लाइंट first-page रेंडर का diff" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_diff.webp" style={{ margin: "8px auto", height: 85, display: "block" }} />
आँकड़ों में
आपको इस कार्यान्वयन में क्या-क्या शामिल था इसका अंदाजा देने के लिए:
- 2 दिन का काम (शुरू से काम करने वाले SSG तक). यह छुट्टी पर 24 घंटे से थोड़ा अधिक था.
- 4 दिन का काम हाइड्रेशन को बिना async अनुवाद रेस या
useMediaQuery चीजों को बिगाड़े बिना अच्छी तरह से व्यवहार करवाने के लिए.
- 1 अतिरिक्त दिन dehydration दृष्टिकोण को critical path Suspense boundary replacement से बदलने के लिए (सरल, कम बाइट्स, कोई संभावित flash नहीं).
- ~200 लाइनें कोर SSG जनरेशन कोड की (
GenerateShellSsgFromSitemap.ts)
- ~120 लाइनें Suspense boundary रिज़ॉल्यूशन की (
renderRoute.tsx में resolveSuspenseBoundaries) - नोट: इसे बाद में critical path दृष्टिकोण से बदल दिया गया
- ~50 लाइनें SSR यूटिलिटीज़ की (
isSSRMode.ts)
- ~100 लाइनें टेस्ट की (
renderRoute.test.ts)
- ~150 लाइनें SSR के लिए polyfills की (
setupSSREnvironment)
- न्यूनतम बदलाव मौजूदा कंपोनेंट्स में (ज्यादातर
useIsSSRMode() चेक्स जोड़ना)
समाधान हल्का और मेंटेनेबल है. इसके लिए फ्रेमवर्क माइग्रेशन की आवश्यकता नहीं है, और यह हमारे मौजूदा React SPA के साथ काम करता है.
मुख्य निष्कर्ष
कभी-कभी कस्टम समाधान बेहतर होता है
हर समस्या को फ्रेमवर्क की आवश्यकता नहीं होती. Foony के लिए, एक छोटा, कस्टम SSG समाधान सही विकल्प था. यह:
- हल्का: कोई भारी निर्भरता या फ्रेमवर्क ओवरहेड नहीं
- मेंटेनेबल: सरल कोड जिसे हम समझते हैं
- लचीला: आवश्यकतानुसार संशोधित और विस्तारित करना आसान
- संगत: हमारे मौजूदा React SPA के साथ बिना माइग्रेशन के काम करता है
React के स्ट्रीमिंग SSR में विचित्रताएँ हैं
React का renderToReadableStream Suspense से निपटने के लिए अच्छा है, लेकिन इसमें विचित्रताएँ हैं. await stream.allReady के साथ भी, आपको आउटपुट में Suspense boundaries मिलते हैं. यह बग नहीं है, यह स्ट्रीमिंग के लिए डिज़ाइन है. लेकिन SSG के लिए, हमें पूरी तरह से हल किए गए HTML की जरूरत है. यह React टीम की एक विफलता जैसा लगता है कि वे इस परिदृश्य को साफ तरीके से हैंडल नहीं करते.
मेरा समाधान HTML को पोस्ट-प्रोसेस करना और boundaries को हल करना था. यह सुंदर नहीं है, लेकिन यह मेरे उपयोग के मामले के लिए तेज और लचीला है.
TDD LLMs के लिए उपयोगी हो सकता है
HTML ट्रांसफॉर्मेशन त्रुटि-प्रवण है. एक छोटा बग और आप पूरे SSG आउटपुट को तोड़ सकते हैं और एंड-यूजर अनुभव को बिगाड़ सकते हैं. मैंने यह सुनिश्चित करने के लिए एक LLM से व्यापक टेस्ट लिखवाए (मेरे इनपुट के साथ) कि ट्रांसफॉर्मेशन सही ढंग से काम करे.
निष्कर्ष
SSG अब Foony के लिए काम कर रहा है. पेज सर्च इंजन और LLMs के लिए पूरी तरह से रेंडर किए जाते हैं, और समाधान मेंटेनेबल और हल्का है. SSG रूट्स के लिए हाइड्रेशन में मेरी अपेक्षा से अधिक समय लगा (3 दिन), और मैंने प्रारंभिक dehydration दृष्टिकोण को critical path Suspense boundary replacement से बदलने में एक अतिरिक्त दिन बिताया. नया दृष्टिकोण मेंटेन करने में सरल है, कम बाइट्स शिप करता है, और HTML को dehydrate/rehydrate करने से संभावित विज़ुअल flashes को रोकता है.
मैं अभी भी हैरान हूँ कि SSG के लिए एक कस्टम समाधान लागू करने में केवल 2 दिन लगे. लेकिन कभी-कभी सही समाधान सबसे सरल होता है.
भविष्य के काम में हाइड्रेशन मैचिंग को पूरा करना और संभवतः बेहतर डिबगिंग के लिए React को पैच करना शामिल है. लेकिन अभी के लिए, Foony के पास काम करने वाला SSG है. मैं आने वाले हफ्तों में Google Search Console और Bing Webmaster Tools पर नजर रखूँगा यह देखने के लिए कि इसका हमारे SEO पर क्या प्रभाव पड़ता है.