एक डिप्लॉयमेंट के बाद प्रोडक्शन में मिली सबसे उपयोगी त्रुटियों में से एक देखने में लगभग मामूली थी:
Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js
वह उसी त्रुटि-धारा में आई जिसमें एनालिटिक्स और विज्ञापन स्क्रिप्ट की विफलताएँ, सामान्य Script error. संदेश और बीच में रुका वीडियो प्लेबैक भी था। उनमें से अधिकांश मॉनिटरिंग का शोर थे। यह मामला अलग था, क्योंकि जो संसाधन लोड नहीं हुआ वह मेरी अपनी Next.js एप्लिकेशन का हिस्सा था। अगर ब्राउज़र सचमुच उसे नहीं ला पा रहा था, तो पेज का कोई हिस्सा काम करना बंद कर सकता था।
लेकिन उस लॉग से कारण साबित नहीं होता था। यह अस्थायी नेटवर्क समस्या हो सकती थी, प्रॉक्सी या CDN की विफलता हो सकती थी, फ़ाइल वास्तव में गायब हो सकती थी, या कोई पुराना पेज पिछले डिप्लॉयमेंट का चंक मांग रहा हो जबकि सर्वर पहले ही नए बिल्ड पर जा चुका हो।
आख़िरी स्थिति को कम करके आंकना आसान है, क्योंकि नया डिप्लॉयमेंट पूरी तरह स्वस्थ हो सकता है। हर नया विज़िटर मौजूदा वर्ज़न पाता है और सब ठीक चलता है, लेकिन कई घंटे से खुला टैब व्यवहार में अभी भी पिछली रिलीज़ का क्लाइंट बना रहता है।
यह लेख उसी संगतता-अंतर के बारे में है: डिप्लॉयमेंट के बाद पुराने Next.js टैब क्यों टूट सकते हैं, पुराना HTML और गायब /_next/static एसेट कैसे वर्ज़न स्क्यू पैदा करते हैं, आक्रामक सफ़ाई समस्या को क्यों बढ़ाती है, और रिटेंशन, डिप्लॉयमेंट क्रम, मॉनिटरिंग तथा रिकवरी को कैसे व्यवस्थित किया जाए ताकि सफल रिलीज़ पहले से खुले सेशन को न तोड़े।
पहला सबक: हर स्क्रिप्ट विफलता डिप्लॉयमेंट बग नहीं होती
मूल त्रुटि-धारा में कई बिल्कुल अलग तरह की समस्याएँ मिली हुई थीं। थर्ड-पार्टी एनालिटिक्स और विज्ञापन स्क्रिप्ट कंटेंट ब्लॉकर, DNS फ़िल्टर, ब्राउज़र की गोपनीयता सुविधाओं, क्षेत्रीय फ़िल्टरिंग, एंटीवायरस या उपयोगकर्ता के नेटवर्क के कारण ब्लॉक हो सकती हैं। वीडियो का play() प्रॉमिस बाद में आए pause() से रुक सकता है, जबकि एप्लिकेशन पूरी तरह ठीक हो। क्रॉस-ओरिजिन Script error. भी अक्सर निदान के लिए पर्याप्त जानकारी नहीं देता।
लेकिन अपनी एप्लिकेशन का Next.js चंक लोड न होना अलग प्राथमिकता मांगता है। उपयोगी सीमा कुछ ऐसी है:
थर्ड-पार्टी संसाधन विफल
-> आम तौर पर टेलीमेट्री या वैकल्पिक कार्यक्षमता प्रभावित
अपनी /_next/static/*.js फ़ाइल विफल
-> एप्लिकेशन का जरूरी कोड उपलब्ध नहीं हो सकता
यह अंतर इसलिए महत्वपूर्ण है क्योंकि बहुत संवेदनशील रिपोर्टर सैकड़ों बेकार संदेशों के बीच वास्तविक घटना छिपा सकता है। मेरे मामले में ध्यान देने लायक संसाधन /_next/static/chunks/9253.647385b4be0958e4.js था। लॉग ने साबित किया कि फ़र्स्ट-पार्टी स्क्रिप्ट लोड विफल हुआ। उसने यह साबित नहीं किया कि कारण डिप्लॉयमेंट का वर्ज़न स्क्यू था।
मैं प्रमाण की यह सीमा जानबूझकर साफ रखता हूँ: संभावित कारण, पुष्ट कारण नहीं होता।
खुला ब्राउज़र टैब व्यवहार में पुरानी रिलीज़ का क्लाइंट है
जिस मानसिक मॉडल ने समस्या मेरे लिए साफ की, वह सरल है: डिप्लॉयमेंट के बाद एप्लिकेशन के एक से अधिक वर्ज़न एक ही समय पर सक्रिय रह सकते हैं।
मान लें रिलीज़ A सुबह 10:00 बजे लाइव है। उपयोगकर्ता कोई पेज खोलता है और उस रूट के लिए HTML तथा JavaScript प्राप्त करता है। 10:30 पर रिलीज़ B डिप्लॉय होती है। नए विज़िटर B पाते हैं। लेकिन पहले से खुला टैब सिर्फ सर्वर बदल जाने से अपने-आप B नहीं बन जाता।
उस टैब में अभी भी यह सब हो सकता है:
- रिलीज़ A का JavaScript रनटाइम;
- A द्वारा बनाए गए रूट और चंक संदर्भ;
- A से पहले ही प्रीफ़ेच किया गया नेविगेशन डेटा;
- A के दौरान बनी React स्टेट;
- A से पहले ही डाउनलोड हो चुके कोड-विभाजित मॉड्यूल;
- A के ऐसे मॉड्यूल के संदर्भ जो अभी तक डाउनलोड नहीं हुए हैं।
आख़िरी बिंदु अक्सर विफलता को पहली बार दिखाई देता है।
अगर उपयोगकर्ता को आगे जिन सभी चंक की जरूरत होगी वे पहले से ब्राउज़र कैश में हैं, तो सेशन बिना समस्या चल सकता है। लेकिन आधुनिक Next.js एप्लिकेशन कोड को हिस्सों में बाँटते हैं। रूट बदलना, dynamic import, मोडल, एडिटर या बाद में इस्तेमाल होने वाली कोई सुविधा नया JavaScript फ़ाइल मांग सकती है। पुराना रनटाइम तब वह URL मांगता है जो रिलीज़ A में वैध था।
अगर सर्वर वह एसेट अभी भी रखता है, तो सब सामान्य चल सकता है। अगर डिप्लॉयमेंट ने उसे हटा दिया, तो पुराना क्लाइंट 404 पा सकता है, जबकि रिलीज़ B नए उपयोगकर्ताओं के लिए पूरी तरह स्वस्थ हो।
कंटेंट-हैश वाले चंक लंबे कैश के लिए ही बनाए जाते हैं
Next.js सचमुच अपरिवर्तनीय एसेट को जानबूझकर लंबे समय तक कैश करने देता है। मौजूदा स्व-होस्टिंग दस्तावेज़ के अनुसार, जिन अपरिवर्तनीय एसेट के फ़ाइलनाम में SHA हैश होता है उन्हें एक वर्ष की नीति के साथ सर्व किया जाता है, जैसे:
Cache-Control: public, max-age=31536000, immutable
यह समझदारी है: कंटेंट बदलता है तो URL बदलता है। ब्राउज़र को हर अनुरोध पर उसी फ़ाइल को फिर से सत्यापित करने की जरूरत नहीं रहती।
लेकिन इसका एक महत्वपूर्ण परिणाम है: पुराना URL तब तक अर्थपूर्ण रहता है जब तक कोई पुराना दस्तावेज़ या रनटाइम उसे संदर्भित कर सकता है।
ब्राउज़र किसी एसेट को एक साल तक कैश कर सकता है, पर इससे तब कोई मदद नहीं मिलती जब उसने वह खास चंक डिप्लॉयमेंट से पहले कभी डाउनलोड ही नहीं किया हो और बाद में उसे मांगे, जबकि ओरिजिन फ़ाइल हटा चुका हो।
इसलिए “हमारे स्टैटिक एसेट अपरिवर्तनीय हैं” और “हम पिछली रिलीज़ के स्टैटिक एसेट तुरंत हटा सकते हैं” एक ही बात नहीं हैं। अपरिवर्तनीयता पुराने एसेट को सुरक्षित रूप से रखने देती है। वह पुराने क्लाइंट को उन्हें मांगना बंद नहीं कराती।
मौजूदा Next.js स्व-होस्टिंग मार्गदर्शिका मल्टी-सर्वर और चरणबद्ध डिप्लॉयमेंट के दौरान गायब JavaScript या CSS एसेट को वर्ज़न स्क्यू के संभावित लक्षणों में गिनती है। यही समस्या पुराने टैब और नए ओरिजिन के बीच भी हो सकती है।
वर्ज़न कई अलग तरीकों से बँट सकते हैं
“कैश की समस्या” निदान के लिए बहुत अस्पष्ट है। मैं कम से कम चार अलग तंत्र मानता हूँ, क्योंकि उनके समाधान भी अलग हैं।
1. पुराना टैब ऐसा एसेट मांगता है जो डिप्लॉयमेंट से पहले लोड नहीं हुआ था
यह लंबे समय से खुले टैब का क्लासिक मामला है। दस्तावेज़ और रनटाइम A से आए हैं। B सर्वर की फ़ाइलों को बदल देती है। बाद में उपयोगकर्ता ऐसी सुविधा खोलता है जिसे A का देर से लोड होने वाला चंक चाहिए। अगर वह चंक हटा दिया गया है, तो अनुरोध विफल हो जाता है।
2. पुराना HTML ऐसे चंक की ओर इशारा करता है जो अब मौजूद नहीं हैं
CDN, रिवर्स प्रॉक्सी, सर्विस वर्कर, ब्राउज़र कैश या स्टैटिक होस्टिंग की कोई परत पुराने HTML को उम्मीद से अधिक समय तक रख सकती है। वह HTML अभी भी A के एसेट संदर्भित करता है, जबकि ओरिजिन पर केवल B बचा है।
खतरा तब बढ़ता है जब HTML को गलती से हैश वाले एसेट जैसी लंबी immutable नीति मिल जाए। चंक को लंबे समय तक कैश करना ठीक है क्योंकि उसका URL कंटेंट को वर्ज़न करता है। HTML ही तय करता है कि कौन-से चंक URL एक साथ एक सुसंगत रिलीज़ बनाते हैं।
3. रोलिंग या मल्टी-इंस्टेंस डिप्लॉयमेंट अलग रिलीज़ मिला देता है
लोड बैलेंसर के पीछे दो Next.js इंस्टेंस सोचिए। एक B पर है, दूसरा अभी A पर। दस्तावेज़ एक वर्ज़न से आ सकता है और अगला नेविगेशन अनुरोध दूसरे इंस्टेंस पर जा सकता है। मौजूदा Next.js दस्तावेज़ इसे वर्ज़न असंगति कहते हैं और गायब एसेट, सर्वर फ़ंक्शन्स असंगति तथा नेविगेशन विफलताओं को इसके संभावित परिणाम बताते हैं।
सुरक्षित आधार यह है कि एक लॉजिकल रिलीज़ को एक बार बिल्ड किया जाए और वही बिल्ड आर्टिफ़ैक्ट सभी इंस्टेंस पर चलाया जाए। Next.js स्व-होस्टिंग दस्तावेज़ भी कंटेनर के बीच वही बिल्ड और सुसंगत बिल्ड आईडी इस्तेमाल करने की सलाह देते हैं, न कि हर रेप्लिका पर स्वतंत्र बिल्ड चलाने की।
4. डिप्लॉयमेंट फ़ाइलों को गलत क्रम में प्रकाशित करता है
पुराने टैब के बिना भी गैर-एटॉमिक डिप्लॉयमेंट थोड़े समय के लिए असंभव स्थिति बना सकता है:
नया HTML दिखाई दे रहा है
+
नए चंक अभी उपलब्ध नहीं हैं
या उल्टा:
पुराना HTML अभी दिखाई दे रहा है
+
पुराने चंक पहले ही हटाए जा चुके हैं
यह खिड़की बहुत छोटी हो तब भी काफी है। उपयोगकर्ता को बस एक बार उसी समय आना होता है।
खतरनाक पैटर्न है: “सब बदलो और पुरानी ट्री हटा दो”
सरल डिप्लॉयमेंट स्क्रिप्ट अक्सर कुछ ऐसी शुरू होती है:
build
rsync --delete new-output/ production/
restart
यह आकर्षक है क्योंकि प्रोडक्शन डायरेक्टरी हमेशा सबसे नए बिल्ड से बिल्कुल मेल खाती है। लेकिन लंबे समय से खुले क्लाइंट के लिए यह खराब व्यवहार है।
हैश वाले स्टैटिक एसेट में पुरानी फ़ाइलें आम तौर पर नई फ़ाइलों से टकराती नहीं हैं, क्योंकि उनके URL अलग होते हैं। उन्हें तुरंत हटाने से मुख्यतः डिस्क स्पेस बचती है। बदले में पुराने क्लाइंट का हर अभी भी वैध संदर्भ संभावित 404 बन जाता है।
अब मैं पुराने चंक को डिप्लॉयमेंट-संगतता का हिस्सा मानता हूँ, कचरा नहीं।
इसका मतलब हर रिलीज़ हमेशा रखना नहीं है। इसका मतलब सिर्फ इतना है कि सफ़ाई अलग रिटेंशन नीति होनी चाहिए, नई रिलीज़ को सक्रिय करने का आकस्मिक साइड इफ़ेक्ट नहीं।
रिटेंशन उपयोगी है, लेकिन कोई सीमित अवधि पूरा समाधान नहीं
सेल्फ-होस्टेड सेटअप पुराने /_next/static एसेट को एक रियायती अवधि तक रख सकता है। सही अवधि वर्कलोड पर निर्भर करती है। ऐसा पेज जिसे उपयोगकर्ता दो मिनट पढ़कर बंद कर देता है, पूरे कार्यदिवस खुली रहने वाली वेब एप्लिकेशन जैसा नहीं है।
न्यूनतम रिटेंशन के बारे में सोचने का एक उपयोगी तरीका:
रिटेंशन अवधि >=
पुराने HTML की अपेक्षित उम्र
+ लंबे समय तक खुले टैब की व्यावहारिक अवधि
+ रोलबैक अवधि
+ डिप्लॉयमेंट फैलने का अतिरिक्त समय
यह गणितीय गारंटी नहीं है। टैब कई हफ्ते खुला रह सकता है। घंटों की कोई सीमित संख्या पुराने क्लाइंट की विफलताओं को पूरी तरह असंभव नहीं बनाती।
इसीलिए मुझे परतदार डिज़ाइन बेहतर लगता है:
- पिछली रिलीज़ के अपरिवर्तनीय एसेट इतनी देर रखें कि सामान्य पुराने सेशन चलते रहें;
- वर्ज़न स्क्यू पहचानें ताकि क्लाइंट मौजूदा रिलीज़ पर जा सके;
- एसेट सचमुच उपलब्ध न हो तो सुरक्षित, एक बार की रीलोड या साफ दिखाई देने वाला रिकवरी रास्ता रखें;
- फ़र्स्ट-पार्टी चंक के गायब होने को मॉनिटर करें ताकि रिटेंशन वास्तविक डेटा से तय किया जा सके।
रिटेंशन अधिकांश विफलताएँ रोकता है। रिकवरी उस लंबे अंतिम हिस्से को संभालती है जिसे कोई सीमित रिटेंशन अवधि पूरी तरह खत्म नहीं कर सकती।
पुराने चंक को केवल उम्र देखकर साफ न करें
“सात दिन से पुरानी हर फ़ाइल हटा दो” जैसी साधारण नीति भी गलत हो सकती है। मौजूदा रिलीज़ किसी पुराने हैश वाली फ़ाइल को फिर से इस्तेमाल कर सकती है जिसका कंटेंट नहीं बदला, इसलिए डिस्क पर उसका संशोधन समय पुराना है जबकि फ़ाइल अब भी जरूरी है।
बेहतर सफ़ाई नीति रिलीज़ की जानकारी को ध्यान में रखेगी:
- संगतता अवधि में आने वाली हर रिलीज़ के मैनिफ़ेस्ट या एसेट-इन्वेंटरी रखें;
- उन रिलीज़ द्वारा संदर्भित सभी एसेट पाथ का संयुक्त सेट बनाएं;
- उस सुरक्षित सेट की कोई फ़ाइल न हटाएं;
- केवल असंदर्भित एसेट को अतिरिक्त रियायती अवधि के बाद हटाएं।
अगर छोटे डिप्लॉयमेंट के लिए यह व्यवस्था बहुत भारी है, तो जानबूझकर थोड़ा बड़ा स्टैटिक-एसेट स्टोर अक्सर किसी दुर्लभ प्रोडक्शन विफलता को डिबग करने से सस्ता पड़ता है। हैश वाली फ़ाइलें इसके लिए खास तौर पर अच्छी हैं, क्योंकि एक ही कंटेंट स्थिर URL दोबारा इस्तेमाल कर सकता है या कम-से-कम अलग कंटेंट को उसी हैश वाले नाम से ओवरराइट नहीं कर सकता।
जिस नियम से मैं बचता हूँ वह सरल है: साझा /_next/static ट्री पर --delete को उसी ऑपरेशन का हिस्सा न बनाएं जो नई रिलीज़ को सक्रिय करता है।
Next.js में अब वर्ज़न स्क्यू की स्पष्ट सुरक्षा है, पर वह पुराने एसेट का स्टोरेज नहीं
मौजूदा Next.js वर्ज़न असंगति से सुरक्षा के लिए deploymentId का समर्थन करता है। कॉन्फ़िगरेशन ऐसा हो सकता है:
// next.config.js
const nextConfig = {
deploymentId: process.env.DEPLOYMENT_VERSION,
}
module.exports = nextConfig
Next.js के मौजूदा deploymentId दस्तावेज़ के अनुसार, इसे सेट करने पर फ़्रेमवर्क-प्रबंधित स्थिर एसेट URL में ?dpl=<deploymentId> जुड़ता है, क्लाइंट-साइड नेविगेशन अनुरोध डिप्लॉयमेंट की जानकारी लेकर जाते हैं और सर्वर जवाब में अपनी डिप्लॉयमेंट पहचान बताता है। नेविगेशन के दौरान असंगति मिलने पर Next.js असंगत डेटा के साथ आंशिक नेविगेशन जारी रखने के बजाय पूर्ण नेविगेशन कर सकता है।
?dpl=<deploymentId>
x-deployment-id
x-nextjs-deployment-id
data-dpl-id
यह उपयोगी है, लेकिन इसे उसकी वास्तविक क्षमता से अधिक नहीं समझना चाहिए। दस्तावेज़ साफ कहते हैं कि Next.js आने वाले ?dpl= पैरामीटर का इस्तेमाल वर्ज़न-जागरूक रूटिंग के लिए नहीं करता; वह कैश तोड़ना के लिए है। अगर सेल्फ-होस्टेड ओरिजिन पुराना एसेट भौतिक रूप से हटा चुका है, तो क्वेरी पैरामीटर फ़ाइल वापस नहीं बनाता।
इसलिए मैं deploymentId को वर्ज़न असंगति पहचानने और उससे उबरने का तंत्र मानता हूँ, पुराने एसेट रखने या साफ़-सुथरी डिप्लॉयमेंट प्रक्रिया का विकल्प नहीं।
जिन प्लैटफ़ॉर्म में डिप्लॉयमेंट-सचेत रूटिंग है वे इससे आगे जा सकते हैं। उदाहरण के लिए, Vercel का वर्ज़न-असंगति सुरक्षा दस्तावेज़ वर्ज़न लॉकिंग समझाता है, ताकि फ़्रेमवर्क-प्रबंधित अनुरोध उसी डिप्लॉयमेंट तक पहुँचते रहें जिसने क्लाइंट को पहली बार सेवा दी थी। यह प्लैटफ़ॉर्म की क्षमता है; किसी सामान्य Nginx या CDN सेटअप में इसे अपने-आप मौजूद नहीं माना जा सकता।
बिल्ड आईडी और डिप्लॉयमेंट आईडी जुड़े हुए हैं, लेकिन एक ही चीज़ नहीं
Next.js next build के दौरान बिल्ड आईडी भी बनाता है। अगर कई कंटेनर एक ही डिप्लॉयमेंट सर्व करने वाले हैं, तो उन्हें अलग-अलग बिल्ड चलाकर चुपचाप अलग आउटपुट नहीं बन जाने चाहिए।
एक निर्धारित बिल्ड आईडी को Git कमिट जैसे रिलीज़ पहचानकर्ता से जोड़ा जा सकता है:
// next.config.js
const nextConfig = {
generateBuildId: async () => process.env.GIT_SHA,
deploymentId: process.env.GIT_SHA,
}
module.exports = nextConfig
यह उदाहरण केवल समझाने के लिए है, मेरे प्रोडक्शन कोड से कॉपी नहीं किया गया। महत्वपूर्ण वास्तु-नियम यह है कि एक लॉजिकल रिलीज़ के सभी इंस्टेंस एक ही सुसंगत बिल्ड आर्टिफ़ैक्ट और एक डिप्लॉयमेंट पहचान इस्तेमाल करें।
generateBuildId Next.js बिल्ड की पहचान करता है। deploymentId खास तौर पर वर्ज़न-असंगति सुरक्षा और पुराने कैश को निष्प्रभावी करने के लिए दस्तावेज़ित है। दोनों जुड़े हुए हैं, लेकिन उनके नामों को समानार्थी मानना डिबगिंग कठिन बनाता है।
मैं नए दस्तावेज़ से पहले उसके एसेट प्रकाशित करूँगा
सुरक्षित डिप्लॉयमेंट क्रम जानबूझकर असममित होता है। नए अपरिवर्तनीय एसेट किसी संदर्भ से पहले मौजूद हो सकते हैं। नया HTML तब तक दिखाई नहीं देना चाहिए जब तक उसके सभी जरूरी एसेट उपलब्ध न हों।
सैद्धांतिक क्रम:
1. रिलीज़ B को एक बार बिल्ड करें
2. B के /_next/static एसेट अपलोड करें
3. जाँचें कि जरूरी एसेट सचमुच मिल रहे हैं
4. B का सर्वर या रनटाइम तैयार करें
5. स्वास्थ्य जाँच चलाएं
6. नए दस्तावेज़ का ट्रैफ़िक एक ही स्विच में B पर भेजें
7. A के स्टैटिक एसेट उपलब्ध रखें
8. B को मॉनिटर करें
9. पुराने एसेट बाद में साफ करें
स्टैटिक एक्सपोर्ट में भी यही सिद्धांत लागू होता है: वर्ज़न वाले एसेट पहले अपलोड करें और उन्हें संदर्भित करने वाला HTML बाद में प्रकाशित करें। SSR रिवर्स प्रॉक्सी के पीछे हो तो नया सर्वर पहले तैयार और जाँच लें, फिर ट्रैफ़िक स्विच करें।
रोलबैक भी इसी तरह सममित होना चाहिए। पिछली रिलीज़ की डायरेक्टरी और उसके एसेट रखने से पुरानी फ़ाइलें फिर से बनाने की कोशिश किए बिना एप्लिकेशन रोलबैक संभव रहता है।
इससे हर रोलबैक सुरक्षित नहीं हो जाता। असंगत डेटाबेस माइग्रेशन या API अनुबंध पुरानी एप्लिकेशन को तोड़ सकता है, भले उसका JavaScript अब भी उपलब्ध हो। एसेट रिटेंशन केवल स्टैटिक संगतता की समस्या हल करता है, पूरे सिस्टम की हर रिलीज़-संगतता समस्या नहीं।
सरल स्व-होस्टिंग में साझा अपरिवर्तनीय एसेट डायरेक्टरी अच्छी तरह काम करती है
छोटे Nginx-आधारित डिप्लॉयमेंट में मौजूदा एप्लिकेशन रिलीज़ को साझा स्टैटिक-एसेट स्टोर से अलग किया जा सकता है।
उदाहरण लेआउट:
/srv/app/releases/2026-08-13-a/
/srv/app/releases/2026-08-13-b/
/srv/app/current -> /srv/app/releases/2026-08-13-b/
/srv/app/shared/_next/static/...
हर डिप्लॉयमेंट नए /_next/static एसेट साझा डायरेक्टरी में जोड़ता है, लेकिन रिटेंशन अवधि में आने वाली पुरानी रिलीज़ की फ़ाइलें नहीं हटाता। Nginx इस पाथ को अपरिवर्तनीय नीति के साथ सर्व कर सकता है:
location ^~ /_next/static/ {
root /srv/app/shared;
add_header Cache-Control "public, max-age=31536000, immutable";
}
यह कॉन्फ़िगरेशन केवल उदाहरण है, मेरी सटीक Nginx कॉन्फ़िगरेशन का दावा नहीं। वास्तविक डिप्लॉयमेंट में अनुमतियाँ, MIME प्रकार, संपीड़न प्रकार, CDN व्यवहार और सटीक आउटपुट लेआउट भी देखना होगा।
मुख्य वास्तु-बिंदु जीवनचक्र का है: मौजूदा रिलीज़ की ओर इशारा करने वाला बदलने योग्य संकेतक और वर्ज़न वाले एसेट का मुख्यतः केवल जुड़ता रहने वाला भंडार अलग जीवनचक्र रखते हैं।
HTML की कैश नीति हैश वाले चंक से अलग होनी चाहिए
इस समस्या को दोबारा पैदा करने का सबसे आसान तरीका है HTML को ऐसे कैश करना जैसे वह खुद सामग्री-हैश वाला अपरिवर्तनीय एसेट हो।
डायनेमिक Next.js पेज में फ़्रेमवर्क आम तौर पर उपयोगकर्ता-विशिष्ट डायनेमिक आउटपुट के लिए दोबारा इस्तेमाल न होने वाली प्रतिक्रिया नियम रखता है। स्टैटिक और ISR पेज की नीतियाँ अलग हैं और CDN उन्हें सही तरीके से कैश कर सकता है। Nginx से सर्व किए गए स्थिर निर्यात में व्यवहार ऑपरेटर द्वारा सेट किए गए हेडर पर और अधिक निर्भर होता है।
इसीलिए मैं “पूरी वेबसाइट” के लिए एक ही कैश नियम नहीं रखता। मैं ऑब्जेक्ट की श्रेणियाँ अलग मानता हूँ:
हैश वाला /_next/static एसेट
लंबा max-age
immutable
रखना सुरक्षित
HTML / रूट दस्तावेज़
नई रिलीज़ पर जा सकना चाहिए
नीति रेंडरिंग मॉडल पर निर्भर
जिन एसेट को संदर्भित करता है उनसे अधिक पुराना नहीं रहना चाहिए
RSC / नेविगेशन / API डेटा
ताज़गी और संगतता के अलग नियम
CDN होने पर कैश डिज़ाइन के अनुसार डिप्लॉयमेंट के बाद नए दस्तावेज़ का रूट कैश से हटाना पड़ सकता है। लेकिन सिर्फ नई रिलीज़ आने के कारण पुराने हैश वाले चंक कैश से हटाना उल्टा नुकसान कर सकता है: अगर ओरिजिन ने भी उन्हें हटा दिया हो, तो पुराने टैब को बचा सकने वाली आख़िरी कॉपी भी चली जाती है।
Next.js की CDN कैशिंग मार्गदर्शिका यहाँ उपयोगी है, क्योंकि वह पेज कैशिंग और /_next/static के लंबे अपरिवर्तनीय कैश को अलग-अलग समझाता है।
स्वचालित रीलोड रिकवरी का औज़ार है, मुख्य डिप्लॉयमेंट रणनीति नहीं
चंक विफल होने पर आम जवाब होता है: “पेज रीलोड कर दो।” यह अक्सर काम करता है, क्योंकि पूर्ण नेविगेशन मौजूदा दस्तावेज़ लाता है और वह मौजूदा बिल्ड को संदर्भित करता है।
लेकिन हर स्क्रिप्ट त्रुटि पर रीलोड नई समस्याएँ पैदा करता है:
- थर्ड-पार्टी स्क्रिप्ट की विफलता बेकार रीलोड करा सकती है;
- वास्तविक सर्वर आउटेज अनंत रीलोड लूप बना सकता है;
- बिना सहेजे फ़ॉर्म का डेटा खो सकता है;
- पूर्ण नेविगेशन पर React कंपोनेंट स्टेट खत्म हो जाती है;
- मौजूदा रिलीज़ ही टूटी हो तो वही विफलता फिर हो सकती है।
मौजूदा Next.js दस्तावेज़ भी बताते हैं कि स्क्यू रिकवरी के लिए पूर्ण नेविगेशन से useState जैसी कंपोनेंट स्टेट खो सकती है, जबकि URL या स्थायी ब्राउज़र स्टोरेज में रखी स्टेट बच सकती है।
अगर मैं क्लाइंट-साइड रिकवरी जोड़ूँ, तो उसे संकीर्ण और एक बार की रखूँगा। उदाहरण:
const RECOVERY_KEY = 'next-chunk-recovery-attempted'
function isOwnNextAsset(url: string) {
try {
const parsed = new URL(url, window.location.href)
return (
parsed.origin === window.location.origin &&
parsed.pathname.startsWith('/_next/static/')
)
} catch {
return false
}
}
window.addEventListener(
'error',
(event) => {
const target = event.target
if (!(target instanceof HTMLScriptElement)) return
if (!isOwnNextAsset(target.src)) return
reportChunkFailure({
page: window.location.href,
asset: target.src,
})
if (sessionStorage.getItem(RECOVERY_KEY)) return
sessionStorage.setItem(RECOVERY_KEY, '1')
window.location.reload()
},
true,
)
यह केवल उदाहरण है। प्रोडक्शन इम्प्लीमेंटेशन में स्टाइलशीट चंक, फ़्रेमवर्क की ज्ञात त्रुटि-आकृतियाँ, ऐसे वर्कफ़्लो जहाँ रीलोड नुकसानदेह है, और स्वस्थ लोड के बाद रिकवरी मार्कर कैसे हटेगा—ये सब भी देखना होगा।
एडिटर, भुगतान प्रक्रिया या लंबे फ़ॉर्म में जबरन रिफ़्रेश के बजाय “नया वर्ज़न उपलब्ध है; अपना काम सहेजें और फिर रीलोड करें” जैसा बैनर बेहतर हो सकता है।
मॉनिटरिंग पेलोड को बताना चाहिए कि यह सचमुच वर्ज़न स्क्यू है या नहीं
सिर्फ “स्क्रिप्ट लोड नहीं हुई” पर्याप्त नहीं। हटाए गए पुराने चंक और यादृच्छिक नेटवर्क विफलता में अंतर करने के लिए डिप्लॉयमेंट-संबंधी संदर्भ चाहिए।
उपयोगी फ़ील्ड:
- विफल एसेट का URL;
- मौजूदा पेज का URL;
- संसाधन फ़र्स्ट-पार्टी है या थर्ड-पार्टी;
- क्लाइंट को दिखाई देने वाला रिलीज़ या डिप्लॉयमेंट पहचानकर्ता;
- ब्राउज़र और ऑपरेटिंग सिस्टम;
navigator.onLineको केवल कमजोर संकेत की तरह, कनेक्टिविटी के प्रमाण की तरह नहीं;- पेज लोड हुए कितना समय बीता;
- त्रुटि डिप्लॉयमेंट के कितने समय बाद हुई;
- यह रिकवरी की पहली कोशिश थी या नहीं;
- सर्वर-साइड पर दिखने पर HTTP स्थिति कोड;
- ओरिजिन या प्रॉक्सी ने अनुरोध के समय वास्तव में कौन-सी रिलीज़ सर्व की।
इसके बाद पैटर्न कहीं अधिक अर्थपूर्ण हो जाते हैं।
अगर अलग-अलग नेटवर्क के कई उपयोगकर्ता रिलीज़ के तुरंत बाद पुराने हैश वाले चंक URL मांग रहे हैं और ओरिजिन 404 लौटा रहा है, तो पुराने एसेट जल्दी हटा देना एक मजबूत व्याख्या बनती है। अगर केवल एक उपयोगकर्ता बिना HTTP प्रतिक्रिया के नेटवर्क-स्तरीय विफलता देखता है, तो वर्ज़न स्क्यू बहुत कम निश्चित है। अगर चंक 200 लौटाता है लेकिन MIME प्रकार गलत है या जवाब में HTML त्रुटि-पेज है, तो समस्या केवल रिटेंशन नहीं बल्कि रूटिंग या प्रॉक्सी कॉन्फ़िगरेशन है।
मैं फ़र्स्ट-पार्टी चंक विफलताओं की अलर्टिंग थर्ड-पार्टी संसाधन त्रुटियों से अलग भी रखूँगा। मेरी मूल लॉग से सीधे मिलने वाला सबक यही था: महत्वपूर्ण संकेत बहुत अधिक असंबंधित ब्राउज़र शोर में मिला हुआ था।
पुनरुत्पादन परीक्षण सरल है, लेकिन पुराना टैब पुराना ही रहना चाहिए
यह वर्ग सामान्य रिलीज़ परीक्षण में आसानी से छूट जाता है क्योंकि इंजीनियर डिप्लॉयमेंट के तुरंत बाद रिफ़्रेश कर देते हैं। रिफ़्रेश वही स्थिति खत्म कर देता है जिसे हमें जाँचना है।
बेहतर मैन्युअल परीक्षण:
- रिलीज़ A डिप्लॉय करें;
- ब्राउज़र कैश चालू रखते हुए प्रोडक्शन जैसी स्थिति में एक टैब खोलें;
- एप्लिकेशन का केवल कुछ हिस्सा इस्तेमाल करें ताकि कुछ रूट या देर से लोड होने वाली सुविधाएँ अभी लोड न हों;
- टैब खुला छोड़ दें;
- रिलीज़ B डिप्लॉय करें;
- पुराने टैब को रिफ़्रेश न करें;
- ऐसा रूट या डायनेमिक फ़ीचर खोलें जिसे ऐसा कोड चाहिए जो पहले लोड नहीं हुआ था;
- Network और Console देखें;
- जाँचें कि पुराने एसेट URL अभी भी 200 लौटाते हैं या नहीं;
- जाँचें कि जरूरत होने पर वर्ज़न-स्क्यू पहचान नियंत्रित पूर्ण नेविगेशन कराती है या नहीं।
मैं यही परीक्षण CDN के साथ, चरणबद्ध डिप्लॉयमेंट के दौरान दो सर्वर इंस्टेंस के साथ, और रिटेंशन अवधि खत्म होने के बाद भी दोहराऊँगा।
एक छोटी परीक्षण गलती है DevTools में हर चीज़ के लिए “Disable कैश” चालू कर देना। कुछ निदान में यह उपयोगी है, लेकिन इससे ब्राउज़र का व्यवहार बदल जाता है। लंबे समय से खुले टैब की स्थिति को वास्तविक कैशिंग के साथ भी जाँचना चाहिए, क्योंकि ब्राउज़र कैश इस सिस्टम का हिस्सा है।
हर चंक विफलता पुरानी फ़ाइलें रखकर ठीक नहीं होती
रिटेंशन इसलिए शक्तिशाली है क्योंकि वह एक सीमित तंत्र हल करता है। उसे नया सार्वभौमिक स्पष्टीकरण नहीं बनना चाहिए।
फ़र्स्ट-पार्टी चंक इन कारणों से भी विफल हो सकता है:
- अनुरोध सर्वर तक पहुँचा ही नहीं;
- कनेक्शन बीच में टूट गया;
- ब्राउज़र एक्सटेंशन ने उसे ब्लॉक कर दिया;
- CDN किनारे का सर्वर पर अस्थायी विफलता हुई;
- Nginx ने पाथ गलत रूट किया;
- सर्वर ने JavaScript के बजाय HTML त्रुटि-पेज लौटा दिया;
- संपीड़न या
Content-Encodingखराब हो गया; - फ़ाइल अनुमतियाँ गलत थीं;
- अधूरे डिप्लॉयमेंट में चंक अपलोड ही नहीं हुआ;
- फ़ाइल मौजूद थी लेकिन बहुत जल्दी हटा दी गई;
- क्लाइंट और सर्वर असंगत डिप्लॉयमेंट पर थे।
रिस्पॉन्स कोड और समय दोनों मायने रखते हैं। हर रिलीज़ के बाद पुराने सामग्री-हैश वाला URL पर बार-बार आने वाला 404, किसी एक मोबाइल नेटवर्क पर ERR_CONNECTION_RESET से बिल्कुल अलग कहानी बताता है।
इसीलिए मैं मूल घटना को “मैंने साबित कर दिया कि पुराने HTML ने साइट तोड़ी” के रूप में नहीं लिखूँगा। मैंने ऐसा साबित नहीं किया। मैंने वास्तविक फ़र्स्ट-पार्टी चंक विफलता देखी और वर्ज़न स्क्यू को एक गंभीर विफलता-तंत्र के रूप में पहचाना जिसे डिज़ाइन में रोकना उचित है।
सबसे सुरक्षित डिप्लॉयमेंट पुराने क्लाइंट को भी रिलीज़ का हिस्सा मानता है
गहरी गलती यह मानना है कि डिप्लॉयमेंट एक ही क्षण में A को B से बदल देता है।
सर्वर पर प्रतीकात्मक लिंक या कंटेनर ऑर्केस्ट्रेटर ऐसा दिखा सकता है। लेकिन नेटवर्क पर पुराने CDN ऑब्जेक्ट रह सकते हैं। ब्राउज़र में A के दस्तावेज़ B के लाइव होने के बाद भी लंबे समय तक चल सकते हैं। रोलिंग रिलीज़ के दौरान दोनों सर्वर वर्ज़न सक्रिय हो सकते हैं। रोलबैक में B हट सकता है और A फिर मौजूदा बन सकता है।
इसलिए वास्तविक रिलीज़-सतह कोई एक बिंदु नहीं, बल्कि समय की अवधि है।
अब Next.js एप्लिकेशन के लिए मेरे डिप्लॉयमेंट नियम इसी विचार पर आधारित हैं:
- हर लॉजिकल रिलीज़ को एक बार बिल्ड करें। रेप्लिका को चुपचाप अलग बिल्ड आउटपुट न बनाने दें।
- अपरिवर्तनीय एसेट को उन दस्तावेज़ों से पहले प्रकाशित करें जो उन्हें संदर्भित करते हैं।
- पुराने हैश वाले एसेट को जानबूझकर तय संगतता अवधि तक रखें।
- बदलने वाले HTML को हैश वाले चंक जैसी कैश नीति न दें।
- जहाँ डिप्लॉयमेंट मॉडल में वर्ज़न असंगति हो सकती है, वहाँ
deploymentIdइस्तेमाल करें। - प्लैटफ़ॉर्म-स्तरीय वर्ज़न-असंगति सुरक्षा तभी मानें जब होस्टिंग प्लैटफ़ॉर्म सचमुच वर्ज़न-जागरूक रूटिंग देता हो।
- रिकवरी एक बार की हो और उपयोगकर्ता की स्टेट का ध्यान रखे।
- फ़र्स्ट-पार्टी चंक विफलताओं को अलग प्रोडक्शन संकेत की तरह मॉनिटर करें।
- डिप्लॉयमेंट को पुराने खुले टैब के साथ जाँचें।
- पुराने एसेट बाद में साफ करें, रिलीज़ सक्रिय करते समय नहीं।
अब मेरा नियम
सफल बिल्ड और सही खुलता ताज़ा पेज यह साबित नहीं करते कि डिप्लॉयमेंट उन उपयोगकर्ताओं के लिए सुरक्षित है जो पहले से वहाँ थे।
पुराना टैब बेकार अवशेष नहीं है। वह वास्तविक क्लाइंट है जो पिछली वास्तविक रिलीज़ चला रहा है।
जब मैंने डिप्लॉयमेंट को इस तरह देखना शुरू किया, चंक की समस्या कम रहस्यमय हो गई। कंटेंट हैश एसेट को स्थिर पहचान देता है। लंबे समय का कैश उस पहचान को कुशल बनाता है। लेकिन डिप्लॉयमेंट को उस पहचान का पर्याप्त समय तक सम्मान करना होगा, या क्लाइंट को मौजूदा वर्ज़न पर जाने का नियंत्रित रास्ता देना होगा।
मुझे हर पुरानी रिलीज़ हमेशा जीवित नहीं रखनी। मुझे बस उस अवधि को सुरक्षित निकालना है जिसमें पुराने क्लाइंट और नए सर्वर स्वाभाविक रूप से साथ मौजूद रहते हैं।
अब मेरे लिए डिप्लॉयमेंट अनुबंध यह है: नए उपयोगकर्ता नई रिलीज़ पाएं, पुराने उपयोगकर्ता वे फ़ाइलें न खोएँ जिन्हें उनका मौजूदा वर्ज़न अभी भी मांगना जानता है, और बचा हुआ कोई भी वर्ज़न असंगति टूटा पेज देने के बजाय जानबूझकर बनाए रिकवरी रास्ते में पहुँचे।