بازگشت به بلاگ
۲۲ مرداد ۱۴۰۵Sergei Solod21 دقیقه مطالعه

چرا تب‌های قدیمی Next.js بعد از استقرار خراب می‌شوند: HTML قدیمی، چانک‌های حذف‌شده و ناهمخوانی نسخه‌ها

بعد از یک استقرار، مانیتورینگ محیط تولید من شکست بارگذاری یک چانک JavaScript متعلق به خود برنامه Next.js را ثبت کرد. لاگ وقوع خطا را ثابت می‌کرد، نه علتش را. این مقاله با تکیه بر همان اتفاق، تب‌های قدیمی، HTML قدیمی، فایل‌های گمشده /_next/static، ناهمخوانی نسخه‌ها، دوره نگه‌داری، deploymentId، ترتیب استقرار، مانیتورینگ و بازیابی کنترل‌شده را توضیح می‌دهد.

Next.jsاستقرارناهمخوانی نسخه‌هاکش وبپایداری فرانت‌اندمنابع ایستا

یکی از مفیدترین خطاهایی که بعد از یک استقرار در محیط تولید دیدم، در نگاه اول تقریباً معمولی به نظر می‌رسید:

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 تبدیل نمی‌شود.

آن تب ممکن است هنوز این موارد را داشته باشد:

  • ران‌تایم JavaScript نسخه A؛
  • ارجاع‌های مسیر و چانکی که A ساخته است؛
  • داده ناوبری که A از قبل پیش‌واکشی کرده است؛
  • وضعیت مربوط به React که زیر نسخه A ساخته شده است؛
  • ماژول‌های تقسیم‌شدهٔ کد نسخه 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، پراکسی معکوس، Service Worker، کش مرورگر یا لایه میزبانی ایستا ممکن است 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 قدیمی
    + بازه واقعی تب‌های طولانی‌عمر
    + بازه بازگشت به نسخه قبل
    + حاشیه انتشار استقرار

این تضمین ریاضی نیست. یک تب می‌تواند هفته‌ها باز بماند. هیچ تعداد محدودی ساعت، خطای کلاینت قدیمی را کاملاً ناممکن نمی‌کند.

به همین دلیل طراحی چندلایه را ترجیح می‌دهم:

  1. فایل‌های تغییرناپذیر نسخه‌های قبلی را آن‌قدر نگه دارید که جلسه‌های عادی قدیمی ادامه پیدا کنند؛
  2. ناهمخوانی نسخه را تشخیص دهید تا کلاینت بتواند به نسخه فعلی منتقل شود؛
  3. اگر فایل واقعاً در دسترس نبود، یک بارگذاری مجدد امن و یک‌باره یا مسیر بازیابی واضح داشته باشید؛
  4. گم شدن چانک‌های خود برنامه را مانیتور کنید تا زمان نگه‌داری با داده واقعی تنظیم شود.

لایه نگه‌داری بیشتر خطاها را جلوگیری می‌کند. لایه بازیابی دنباله نادری را پوشش می‌دهد که هیچ بازه محدود نگه‌داری نمی‌تواند کاملاً حذف کند.

چانک‌های قدیمی را فقط بر اساس سن پاک نکنید

قاعده ساده‌ای مثل «هر فایل قدیمی‌تر از هفت روز را حذف کن» هم می‌تواند اشتباه باشد. نسخه فعلی ممکن است فایل هش‌دار قدیمی‌ای را دوباره استفاده کند که محتوا عوض نشده و در نتیجه زمان تغییرش روی دیسک قدیمی است، با اینکه هنوز لازم است.

مدل قوی‌تر برای پاک‌سازی، آگاه از نسخه‌هاست:

  1. فهرست دارایی‌ها یا فهرست فایل‌های همه نسخه‌هایی را که هنوز در بازه سازگاری هستند نگه دارید؛
  2. مجموع همه مسیرهای فایل ارجاع‌شده توسط آن نسخه‌ها را بسازید؛
  3. هیچ فایل داخل این مجموعه محافظت‌شده را حذف نکنید؛
  4. فقط فایل‌های بدون ارجاع را پس از یک دوره ارفاق اضافی حذف کنید.

اگر این سازوکار برای یک استقرار کوچک بیش‌ازحد پیچیده است، یک دایرکتوری استاتیک کمی بزرگ‌تر معمولاً ارزان‌تر از دیباگ کردن خطاهای نادر کلاینت در محیط تولید است. فایل‌های هش‌دار برای این کار مناسب‌اند، چون محتوای تکراری می‌تواند URL پایدار را دوباره استفاده کند یا دست‌کم محتوای متفاوت نمی‌تواند با همان نام هش‌دار روی آن نوشته شود.

قاعده‌ای که از آن دوری می‌کنم ساده است: --delete روی درخت مشترک /_next/static را بخشی از همان عملیاتی نکنید که نسخه جدید را فعال می‌کند.

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. فایل‌های /_next/static مربوط به B را آپلود کن
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ها، variantهای فشرده‌سازی، رفتار CDN و ساختار واقعی خروجی را هم در نظر گرفت.

نکته معماری مهم، تفاوت چرخه عمر است: اشاره‌گر تغییرپذیر به نسخه فعلی و مخزن تقریباً فقط‌افزودنی فایل‌های نسخه‌دار چرخه عمر یکسانی ندارند.

سیاست کش HTML باید با چانک‌های هش‌دار فرق داشته باشد

ساده‌ترین راه بازتولید این مشکل، کش کردن HTML مثل یک فایل تغییرناپذیر دارای هش محتواست.

برای صفحات داینامیک Next.js، فریم‌ورک معمولاً خروجی داینامیک مخصوص کاربر را با قواعد غیرقابل‌استفاده مجدد سرو می‌کند. صفحات استاتیک و ISR سیاست‌های دیگری دارند و CDN می‌تواند آن‌ها را به‌درستی کش کند. در خروجی ایستا که Nginx سرو می‌کند، رفتار بیشتر به headerهایی بستگی دارد که اپراتور تنظیم کرده است.

پس برای «کل وب‌سایت» یک قانون کش واحد نمی‌گذارم. آن را بر اساس نوع شیء جدا می‌کنم:

فایل هش‌دار /_next/static
    max-age طولانی
    immutable
    نگه داشتنش امن است

HTML / سند مسیر
    باید بتواند به نسخه جدید منتقل شود
    سیاست به مدل رندر بستگی دارد
    نباید از فایل‌هایی که به آن‌ها ارجاع می‌دهد قدیمی‌تر بماند

RSC / ناوبری / داده API
    قواعد جداگانه برای تازگی و سازگاری

اگر CDN وجود دارد، بسته به طراحی ممکن است بعد از استقرار لازم باشد مسیر سند جدید پاک‌سازی کش شود. اما پاک‌سازی کش کردن چانک‌های هش‌دار قدیمی فقط چون نسخه جدید آمده، معمولاً نتیجه معکوس دارد: اگر مبدأ هم آن‌ها را حذف کرده باشد، آخرین کپی‌ای که می‌توانست تب قدیمی را نجات دهد از بین می‌رود.

راهنمای کش CDN در Next.js اینجا مفید است، چون کش صفحه را از سیاست بلندمدت و تغییرناپذیر مربوط به /_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 خطا باشد، مشکل مسیریابی یا پیکربندی پراکسی است، نه فقط نگه‌داری فایل.

هشدار خطای چانک خود برنامه را هم از خطاهای منابع شخص ثالث جدا می‌کنم. این مستقیم‌ترین تغییری است که لاگ‌های اولیه من پشتیبانی می‌کنند: سیگنال مهم میان مقدار زیادی نویز نامرتبط مرورگر گم شده بود.

آزمایش بازتولید ساده است، اما تب قدیمی باید واقعاً قدیمی بماند

این نوع خطا در تست معمول انتشار به‌راحتی دیده نمی‌شود، چون مهندس‌ها معمولاً بلافاصله بعد از استقرار صفحه را تازه‌سازی می‌کنند. همین تازه‌سازی شرایطی را که می‌خواهیم آزمایش کنیم از بین می‌برد.

آزمایش دستی بهتر:

  1. نسخه A را مستقر کنید؛
  2. با کش مرورگر فعال، یک تب در محیطی شبیه تولید باز کنید؛
  3. فقط بخشی از برنامه را استفاده کنید تا بعضی مسیرها یا قابلیت‌های تنبل هنوز لود نشده باشند؛
  4. تب را باز نگه دارید؛
  5. نسخه B را مستقر کنید؛
  6. تب قدیمی را صفحه را تازه‌سازی نکنید؛
  7. مسیر یا قابلیت داینامیکی را اجرا کنید که به کدی نیاز دارد که قبلاً لود نشده؛
  8. Network و Console را بررسی کنید؛
  9. ببینید URL فایل‌های قدیمی هنوز 200 می‌دهند یا نه؛
  10. بررسی کنید تشخیص ناهمخوانی نسخه، در صورت نیاز پیمایش کامل کنترل‌شده انجام می‌دهد یا نه.

همین آزمایش را با CDN، با دو نمونه سرور هنگام استقرار تدریجی، و بعد از پایان بازه نگه‌داری هم تکرار می‌کنم.

یک اشتباه ظریف در تست این است که برای همه‌چیز در DevTools گزینه «Disable کش» را روشن کنیم. برای بعضی عیب‌یابی‌ها مفید است، اما رفتار مرورگر را تغییر می‌دهد. سناریوی تب قدیمی باید با کش واقعی هم آزمایش شود، چون کش مرورگر بخشی از سیستم است.

نگه داشتن فایل‌های قدیمی همه خطاهای چانک را حل نمی‌کند

قدرت نگه‌داری در این است که یک سازوکار مشخص را حل می‌کند. نباید خودش به توضیحی عمومی برای هر خطا تبدیل شود.

یک چانک خود برنامه ممکن است به این دلایل شکست بخورد:

  • درخواست اصلاً به سرور نرسیده؛
  • اتصال وسط کار قطع شده؛
  • افزونه مرورگر آن را مسدود کرده؛
  • یک لبه شبکه از CDN موقتاً مشکل داشته؛
  • Nginx مسیر را اشتباه هدایت کرده؛
  • سرور به‌جای JavaScript یک صفحه خطای HTML برگردانده؛
  • فشرده‌سازی یا Content-Encoding خراب شده؛
  • مجوز فایل اشتباه بوده؛
  • استقرار ناقص اصلاً چانک را آپلود نکرده؛
  • فایل وجود داشته اما زودتر از موعد حذف شده؛
  • کلاینت و سرور روی استقرارهای ناسازگار بوده‌اند.

کد پاسخ و زمان‌بندی مهم‌اند. 404 تکرارشونده برای یک URL هش‌دار قدیمی بعد از هر انتشار، داستانی کاملاً متفاوت با ERR_CONNECTION_RESET روی یک شبکه موبایل دارد.

برای همین حادثه اصلی را این‌طور بازنویسی نمی‌کنم که «ثابت کردم HTML قدیمی سایت را خراب کرد». چنین چیزی را ثابت نکردم. یک خطای واقعی چانک خود برنامه دیدم و ناهمخوانی نسخه‌ها را به‌عنوان یکی از حالت‌های جدی شکست شناختم که ارزش دارد در طراحی حذف شود.

امن‌ترین استقرار، کلاینت‌های قدیمی را هم بخشی از سطح انتشار می‌داند

اشتباه عمیق این است که تصور کنیم استقرار در یک لحظه A را با B جایگزین می‌کند.

روی سرور شاید پیوند نمادین یا سامانه هماهنگ‌ساز کانتینر همین را نشان دهد. اما در شبکه ممکن است آبجکت‌های قدیمی CDN باقی بمانند. در مرورگر، سندهای A می‌توانند مدت‌ها بعد از فعال شدن B اجرا شوند. هنگام انتشار تدریجی هر دو نسخه سرور ممکن است هم‌زمان فعال باشند. در بازگشت به نسخه قبل ممکن است B کنار برود و A دوباره نسخه فعلی شود.

پس سطح واقعی انتشار یک بازه زمانی است، نه یک نقطه.

قواعد استقرار من برای برنامه Next.js حالا بر همین ایده بنا شده‌اند:

  • هر نسخه منطقی را یک‌بار بیلد کنید. اجازه ندهید نمونهها بی‌سروصدا خروجی‌های متفاوت بسازند.
  • فایل‌های تغییرناپذیر را قبل از اسنادی منتشر کنید که به آن‌ها ارجاع می‌دهند.
  • فایل‌های هش‌دار قدیمی را برای یک بازه سازگاری مشخص نگه دارید.
  • به HTML قابل تغییر همان سیاست کش چانک‌های هش‌دار را ندهید.
  • وقتی مدل استقرار می‌تواند ناهمخوانی نسخه ایجاد کند، از deploymentId استفاده کنید.
  • فقط وقتی پلتفرم واقعاً مسیریابی آگاه از نسخه دارد، روی حفاظت سطح پلتفرم در برابر ناهمخوانی نسخه حساب کنید.
  • بازیابی را یک‌باره و آگاه از وضعیت کاربر طراحی کنید.
  • خطاهای چانک خود برنامه را به‌عنوان سیگنال جداگانه در محیط تولید مانیتور کنید.
  • استقرار را با یک تب قدیمی که هنوز باز است آزمایش کنید.
  • فایل‌های قدیمی را بعداً پاک کنید، نه هنگام فعال کردن نسخه.

قاعده‌ای که حالا استفاده می‌کنم

بیلد سبز و صفحه تازه‌ای که درست باز می‌شود ثابت نمی‌کنند استقرار برای کاربرانی که از قبل آنجا بوده‌اند امن است.

تب قدیمی زباله باقیمانده نیست. یک کلاینت واقعی است که نسخه قبلی واقعی را اجرا می‌کند.

وقتی استقرار را این‌طور دیدم، مشکل چانک کمتر مرموز شد. هش محتوا به فایل هویتی پایدار می‌دهد. کش بلندمدت این هویت را کارآمد می‌کند. اما استقرار باید آن هویت را مدت کافی حفظ کند، یا راه کنترل‌شده‌ای برای رفتن کلاینت به نسخه فعلی داشته باشد.

لازم نیست هر نسخه قدیمی برای همیشه زنده بماند. فقط لازم است سیستم دوره‌ای را تحمل کند که کلاینت‌های قدیمی و سرورهای جدید به‌طور طبیعی هم‌زمان وجود دارند.

قرارداد استقرار مهم برای من حالا این است: کاربر جدید نسخه جدید را بگیرد، کاربر قدیمی فایل‌هایی را که نسخه فعلی‌اش هنوز بلد است درخواست کند از دست ندهد، و هر ناهمخوانی باقی‌مانده به‌جای صفحه خراب وارد مسیر بازیابی عمدی و کنترل‌شده شود.