العودة إلى المدونة
13 أغسطس 2026Sergei Solod17 دقائق قراءة

لماذا تتعطل تبويبات 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، أو ملفًا مفقودًا فعلًا. وهناك احتمال آخر أكثر ارتباطًا بالنشر: أن تكون صفحة قديمة ما زالت مفتوحة وتطلب ملف JavaScript من الإصدار السابق بعد أن استبدل الخادم ذلك الإصدار.

هذا النوع الأخير سهل الاستخفاف به، لأن النشر الجديد قد يكون سليمًا تمامًا. كل زائر جديد يرى النسخة الجديدة ويعمل الموقع لديه، بينما تبويب ظل مفتوحًا لساعات يبقى عمليًا عميلًا للنسخة القديمة.

هذه المقالة تتناول هذه الفجوة تحديدًا: لماذا قد تتعطل تبويبات Next.js القديمة بعد النشر، وكيف يؤدي HTML القديم أو أصول /_next/static المحذوفة إلى عدم توافق بين الإصدارات، ولماذا يجعل التنظيف العدواني للمخرجات المشكلة أسوأ، وكيف يمكن تصميم الاحتفاظ بالملفات، وآلية النشر، والمراقبة، والاستعادة بحيث لا يترك الإصدار الناجح المستخدمين الموجودين أصلًا أمام صفحة مكسورة.

الدرس الأول: ليس كل فشل في تحميل سكربت مشكلة نشر

سجل الأخطاء الأصلي كان يجمع أنواعًا مختلفة تمامًا من الأعطال. أدوات التحليل والإعلانات التابعة لجهات خارجية قد تُحجب بواسطة مانع إعلانات، أو فلترة DNS، أو خصائص الخصوصية في المتصفح، أو الشبكة، أو برنامج الحماية. وقد يُلغى وعد play() الخاص بالفيديو لأن الكود استدعى pause() بعده مباشرةً، من دون أن يكون التطبيق معطّلًا. أما Script error. القادمة من سكربت خارجي، فكثيرًا ما لا تعطي معلومات تكفي للتشخيص.

فشل ملف JavaScript أولي تابع لـ Next.js يستحق أولوية مختلفة. الحد المفيد ليس «يوجد خطأ JavaScript أو لا يوجد»، بل شيء أقرب إلى:

فشل مورد تابع لطرف ثالث
    -> غالبًا مشكلة قياس أو وظيفة اختيارية

فشل /_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 إضافيًا. عندها يطلب المشغّل القديم عنوانًا كان صالحًا في الإصدار A.

إذا ظل الأصل يحتفظ بذلك الملف، قد يستمر كل شيء طبيعيًا. أما إذا حذفه النشر، فيمكن للعميل القديم أن يحصل على 404 رغم أن الإصدار B نفسه يعمل بشكل ممتاز.

الملفات ذات البصمة في الاسم مصممة أصلًا للكاش طويل المدة

يمنح Next.js الأصول غير القابلة للتغيير فعليًا سياسة تخزين مؤقت طويلة عمدًا. وتذكر وثائق الاستضافة الذاتية الحالية أن الأصول غير القابلة للتغيير التي تتضمن أسماء ملفاتها بصمات SHA تُخدم بسياسة لمدة عام مثل:

Cache-Control: public, max-age=31536000, immutable

وهذا منطقي: عندما يتغير المحتوى يتغير عنوان الملف. لذلك لا يحتاج المتصفح إلى إعادة التحقق من الملف في كل طلب.

لكن هناك نتيجة مهمة: عنوان الملف القديم يظل ذا معنى ما دام يوجد مستند أو مشغّل قديم يمكنه الإشارة إليه.

وجود كاش لمدة سنة لا ينقذ المستخدم إذا لم يكن قد حمّل ذلك الملف بعينه قبل النشر، ثم حاول طلبه لاحقًا بعد أن حذفه الخادم.

لذلك عبارة «الملفات ثابتة وغير قابلة للتغيير» لا تعني «يمكن حذف ملفات الإصدار السابق فورًا». عدم القابلية للتغيير يجعل الاحتفاظ بالملف آمنًا. لكنها لا تجعل العميل القديم يتوقف عن طلبه.

ويذكر دليل الاستضافة الذاتية في Next.js الحالي صراحةً أن فقدان ملفات JavaScript أو CSS من أعراض اختلاف الإصدارات أثناء النشر المتدرج أو العمل على عدة خوادم. المبدأ نفسه ينطبق عندما يكون الاختلاف بين تبويب قديم وأصل جديد.

اختلاف الإصدارات يمكن أن يحدث بأكثر من طريقة

عبارة «مشكلة كاش» واسعة أكثر من اللازم. أفصل بين أربع آليات على الأقل، لأن كل واحدة تحتاج إصلاحًا مختلفًا.

1. تبويب قديم يطلب ملفًا لم يحمّله قبل النشر

هذه حالة التبويب طويل العمر. المستند والمشغّل من الإصدار A. الإصدار B يحل محل الملفات على الخادم. بعد ذلك يفتح المستخدم ميزة لم تُحمّل بعد، فيطلب A ملفًا كسول التحميل. إذا حُذف الملف، يفشل الطلب.

2. HTML قديم يشير إلى ملفات لم تعد موجودة

قد يحتفظ CDN أو الوكيل العكسي أو Service Worker أو كاش المتصفح بنسخة HTML أقدم مما تتوقع. هذه النسخة قد تشير إلى أصول من A بينما الأصل لا يحتوي إلا على B.

تزداد الخطورة إذا حصل HTML بالخطأ على سياسة immutable طويلة. ملف JavaScript الذي يحمل بصمة في عنوانه يمكن تخزينه طويلًا، لأن عنوانه يعرّف محتواه. أما HTML فهو الذي يقرر أي مجموعة من عناوين الملفات يجب أن تعمل معًا.

3. نشر متدرج أو عدة خوادم تخدم نسخًا مختلفة

تخيل مثيلين من Next.js خلف موازن تحميل. أحدهما صار على B والآخر ما زال على A. قد يصل المستند من أحدهما ثم تصل عملية تنقّل لاحقة إلى الآخر. وثائق Next.js الحالية تسمي هذا اختلاف الإصدارات وتذكر أنه قد يؤدي إلى أصول مفقودة، وعدم توافق وظائف الخادم، وفشل التنقل.

القاعدة الآمنة هي بناء الإصدار مرة واحدة وتشغيل البنية نفسها على كل مثيلات النشر المنطقي نفسه. كما توصي وثائق الاستضافة الذاتية في Next.js باستخدام البنية نفسها ومعرّف بناء متسق بين الحاويات بدل إعادة البناء بشكل مستقل على كل خادم.

4. النشر نفسه يظهر الملفات بترتيب خاطئ

حتى من دون تبويبات قديمة، يمكن لرفع غير ذري أن يصنع حالة مستحيلة مؤقتًا:

HTML الجديد أصبح متاحًا
+
ملفات JavaScript الجديدة لم تصبح متاحة بعد

أو العكس:

HTML القديم ما زال متاحًا
+
ملفاته القديمة حُذفت بالفعل

نافذة قصيرة جدًا تكفي. لا يحتاج المستخدم إلى الوقوع فيها إلا مرة واحدة.

نمط النشر الأخطر: استبدال كل شيء وحذف الشجرة القديمة

كثير من سكربتات النشر تبدأ ببساطة من نمط يشبه:

build
rsync --delete new-output/ production/
restart

هو نمط جذاب لأن مجلد الإنتاج يصبح مطابقًا للنسخة الأحدث تمامًا. لكنه سيئ للعملاء الذين ما زالوا يشغّلون إصدارًا سابقًا.

مع الأصول ذات البصمات، وجود ملفات قديمة بجانب الملفات الجديدة لا يسبب تعارضًا عادةً، لأن عناوينها مختلفة. حذف القديم يوفر مساحة تخزين فقط، لكنه في المقابل يحول كل مرجع صالح ما زال موجودًا لدى عميل قديم إلى 404 محتمل.

أصبحت أنظر إلى ملفات الملفات المجزأة القديمة على أنها جزء من توافق النشر، لا قمامة.

هذا لا يعني الاحتفاظ بكل إصدار إلى الأبد. يعني فقط أن التنظيف يجب أن يكون سياسة مستقلة للاحتفاظ، وليس أثرًا جانبيًا لعملية نشر الإصدار الجديد.

الاحتفاظ بالملفات مهم، لكن أي مدة محدودة لن تكون ضمانًا كاملًا

في الاستضافة الذاتية يمكن الاحتفاظ بأصول /_next/static السابقة لفترة سماح. المدة المناسبة تعتمد على طبيعة المنتج. موقع يقرأه المستخدم لدقيقتين يختلف عن تطبيق يظل مفتوحًا طوال يوم العمل.

يمكن التفكير في الحد الأدنى هكذا:

فترة الاحتفاظ >=
    أقصى عمر متوقع لـ HTML القديم
    + مدة واقعية للتبويبات طويلة العمر
    + نافذة الرجوع عن الإصدار
    + هامش انتشار النشر

هذه ليست معادلة تضمن الصفر. قد يبقى تبويب مفتوحًا لأسابيع. لذلك لا توجد مدة محدودة تمنع الخطأ تمامًا.

لهذا أفضل حلًا متعدد الطبقات:

  1. الاحتفاظ بالأصول القديمة غير القابلة للتغيير مدة تكفي للجلسات الطبيعية؛
  2. اكتشاف اختلاف الإصدار كي ينتقل العميل إلى النسخة الحالية؛
  3. وجود إعادة تحميل واحدة وآمنة أو مسار استعادة واضح إذا أصبح الملف غير متاح فعلًا؛
  4. مراقبة 404 الخاصة بأصول التطبيق لضبط فترة الاحتفاظ استنادًا إلى بيانات حقيقية.

الاحتفاظ يمنع معظم الأعطال. والاستعادة تتعامل مع الحالات النادرة التي لا يمكن لأي مدة محددة التخلص منها بالكامل.

لا تنظف الملفات القديمة بقاعدة عمر عمياء

حتى قاعدة مثل «احذف كل ملف أقدم من سبعة أيام» قد تكون خاطئة. الإصدار الحالي قد يعيد استخدام ملف قديم لم يتغير محتواه، وبالتالي تكون بصمته نفسها بينما تاريخ تعديله على القرص قديم.

تنظيف أقوى يكون واعيًا بالإصدارات:

  1. احتفظ بملفات البيان أو قوائم الأصول لكل إصدار ما زال داخل نافذة التوافق؛
  2. أنشئ اتحاد كل المسارات التي تشير إليها تلك الإصدارات؛
  3. لا تحذف أي ملف داخل هذه المجموعة المحمية؛
  4. احذف الملفات غير المشار إليها فقط بعد فترة سماح إضافية.

إذا كان هذا التعقيد مبالغًا فيه لمشروع صغير، فقد يكون الاحتفاظ بمجلد ثابت أكبر قليلًا أرخص بكثير من مطاردة خطأ نادر في الإنتاج.

القاعدة التي أتجنبها هي: لا تجعل --delete على شجرة /_next/static المشتركة جزءًا من عملية ترقية الإصدار نفسها.

Next.js لديه الآن حماية صريحة من اختلاف الإصدارات، لكنها ليست مخزنًا للملفات القديمة

Next.js الحالي يدعم deploymentId للحماية من اختلاف الإصدارات. مثال الإعداد:

// next.config.js
const nextConfig = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
}

module.exports = nextConfig

وفق وثائق deploymentId الحالية، يؤدي ضبطه إلى إضافة Next.js للمعامل ?dpl=<deploymentId> إلى عناوين URL للأصول الثابتة التي يديرها الإطار، وإرفاق معلومات النشر بطلبات التنقل من جهة العميل، وإرسال معرّف النشر في استجابات الخادم. وإذا اكتشف 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 الجديد

تسلسل النشر الآمن غير متماثل عمدًا: يمكن أن توجد الأصول الجديدة قبل أن يشير إليها أحد، لكن لا ينبغي أن يظهر 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 كما لو كان ملف JavaScript يحمل بصمة في اسمه.

في الصفحات الديناميكية يستخدم Next.js عادةً سياسات تمنع إعادة استخدام المخرجات الخاصة بالمستخدم. أما الصفحات الثابتة وISR فلها سياسات أخرى، ويمكن للـ CDN أن يخزنها بصورة صحيحة. وفي تصدير ثابت يعتمد كل شيء أكثر على رؤوس الكاش التي يضعها Nginx أو منصة الاستضافة.

لذلك لا أستخدم قاعدة كاش واحدة لكل «الموقع». أفكر بحسب نوع الكائن:

أصل ذو بصمة داخل /_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,
)

هذا مثال لا أكثر. في الإنتاج يجب التفكير أيضًا في ملفات CSS المقسمة، وأشكال أخطاء الإطار، والسيناريوهات التي يكون فيها إعادة التحميل مدمّرًا، وكيفية حذف علامة الاستعادة بعد تحميل ناجح.

في محرر أو عملية شراء أو نموذج طويل قد يكون شريط «هناك إصدار جديد؛ احفظ عملك ثم أعد التحميل» أفضل من إعادة تحميل إجبارية.

بيانات المراقبة يجب أن تخبرني إن كان السبب فعلًا اختلاف إصدار

رسالة «فشل تحميل السكربت» لا تكفي. للتمييز بين ملف مجزأ قديم حُذف وبين مشكلة شبكة عشوائية، أحتاج سياقًا مرتبطًا بالنشر.

الحقول المفيدة:

  • عنوان الأصل الذي فشل؛
  • عنوان الصفحة الحالية؛
  • هل المورد أولي أم تابع لطرف ثالث؛
  • معرّف الإصدار أو النشر الظاهر للعميل؛
  • المتصفح ونظام التشغيل؛
  • navigator.onLine كإشارة ضعيفة لا كدليل اتصال؛
  • المدة منذ فتح الصفحة؛
  • هل وقع الخطأ قريبًا من وقت النشر؛
  • هل هذه أول محاولة استعادة؛
  • HTTP الحالة عندما يمكن رؤيته على الخادم؛
  • الإصدار الذي خدم الطلب على الأصل أو الوكيل.

بعدها تصبح الأنماط ذات معنى.

إذا طلب مستخدمون كثيرون على شبكات ومتصفحات مختلفة URLs قديمة ذات بصمة وأعاد الأصل 404 مباشرة بعد النشر، يصبح حذف الأصول القديمة تفسيرًا قويًا. إذا كان مستخدم واحد فقط يرى فشلًا شبكيًا بلا استجابة HTTP، فالدليل على اختلاف الإصدارات أضعف بكثير. وإذا عاد الملف 200 لكن بمحتوى HTML أو نوع MIME خاطئ، فالمشكلة في التوجيه أو الوكيل.

كما أفصل إنذارات فشل الملفات المجزأة الأولية عن أخطاء الموارد الخارجية. هذا هو التغيير الذي تدعمه سجلاتي الأصلية مباشرةً: الإشارة المهمة كانت مختلطة بضجيج كبير لا يعني أن التطبيق مكسور.

اختبار إعادة الإنتاج بسيط، لكن يجب أن تبقي التبويب القديم حيًا

هذه الفئة من الأخطاء تفلت من اختبار النشر العادي لأن المطور غالبًا يحدّث الصفحة فورًا بعد النشر. بذلك يمحو الشرط الذي يريد اختباره.

اختبار يدوي أفضل:

  1. انشر الإصدار A؛
  2. افتح تبويبًا ببيئة شبيهة بالإنتاج مع كاش المتصفح مفعّلًا؛
  3. استخدم جزءًا فقط من التطبيق حتى تبقى بعض المسارات أو الميزات غير محملة؛
  4. اترك التبويب مفتوحًا؛
  5. انشر الإصدار B؛
  6. لا تحدّث التبويب القديم؛
  7. افتح مسارًا أو ميزة ديناميكية تحتاج كودًا لم يُحمّل سابقًا؛
  8. راقب Network وConsole؛
  9. تحقق هل URLs القديمة ما زالت تعيد 200؛
  10. وتحقق هل اكتشاف الاختلاف ينفذ تنقّل كامل بشكل مضبوط عند الحاجة.

وأكرر الاختبار مع CDN، ومع مثيلين أثناء النشر المتدرج، وبعد انقضاء فترة الاحتفاظ.

ومن الأخطاء الصغيرة تشغيل “Disable cache” في DevTools لكل شيء. قد يكون مفيدًا لتشخيصات أخرى، لكنه يغير السلوك الذي نحاول اختباره. سيناريو التبويب القديم يجب اختباره أيضًا مع كاش واقعي.

الاحتفاظ بالملفات لا يصلح كل فشل ملف مجزأ

قوة الاحتفاظ أنه يحل آلية محددة. لا ينبغي أن يتحول إلى تفسير شامل.

قد يفشل ملف مجزأ أولي لأن:

  • الطلب لم يصل إلى الخادم أصلًا؛
  • الاتصال انقطع؛
  • إضافة في المتصفح حجبته؛
  • CDN واجه خللًا عابرًا؛
  • Nginx وجّه المسار بشكل خاطئ؛
  • الخادم أعاد صفحة خطأ HTML بدل JavaScript؛
  • الضغط أو Content-Encoding تضرر؛
  • صلاحيات الملف خاطئة؛
  • النشر الجزئي لم يرفع الملف أصلًا؛
  • الملف كان موجودًا ثم حُذف مبكرًا؛
  • العميل والخادم يعملان بإصدارين غير متوافقين.

رمز الاستجابة والتوقيت مهمان. 404 متكرر لعنوان ذو بصمة قديم بعد كل نشر يروي قصة مختلفة تمامًا عن ERR_CONNECTION_RESET لدى شبكة هاتف واحدة.

لهذا لن أعيد كتابة الحادثة الأصلية على أنها «أثبتُّ أن HTML القديم كسر الموقع». لم أثبت ذلك. ما رأيته هو فشل حقيقي في ملف مجزأ أولي، ومنه ظهر اختلاف الإصدارات كأحد أخطر السيناريوهات التي يستحق النظام أن يُصمم لمنعها.

النشر الآمن يعامل العملاء القدامى كجزء من سطح الإصدار

المشكلة الأعمق هي تصور أن النشر يستبدل A بـ B في لحظة واحدة.

قد يكون هذا صحيحًا في رابط رمزي على الخادم. لكنه ليس صحيحًا في النظام الكامل. قد تبقى عناصر قديمة في CDN. وقد تستمر صفحات من A في المتصفح لساعات. وأثناء النشر المتدرج قد تخدم نسختان من الخادم معًا. وعند الرجوع إلى الإصدار السابق قد تصبح A هي الحالية مرة أخرى.

لذلك سطح الإصدار الحقيقي فترة زمنية، لا نقطة.

قواعدي لنشر تطبيق Next.js مبنية على هذه الفكرة:

  • ابنِ مرة واحدة لكل إصدار منطقي. لا تجعل النسخ المتوازية تنتج بنيات مختلفة بصمت.
  • انشر الأصول غير القابلة للتغيير قبل نشر المستندات التي تشير إليها.
  • احتفظ بالأصول القديمة ذات البصمات ضمن نافذة توافق مقصودة.
  • لا تمنح HTML المتغير سياسة الكاش نفسها التي تمنحها للـ الملفات المجزأة ذات البصمة.
  • استخدم deploymentId عندما يسمح نموذج النشر بحدوث اختلاف الإصدارات.
  • استخدم حماية المنصة من اختلاف الإصدارات فقط عندما تكون المنصة توفر التوجيه واعيًا بالإصدار فعلًا.
  • اجعل الاستعادة لمرة واحدة ومراعية لحالة المستخدم.
  • راقب فشل الملفات المجزأة الأولية كإشارة إنتاج مستقلة.
  • اختبر النشر مع تبويب قديم ما زال مفتوحًا.
  • نظف الأصول القديمة لاحقًا، لا أثناء ترقية الإصدار.

القاعدة التي أستخدمها الآن

نجاح بناء وفتح الصفحة الجديدة بشكل سليم لا يثبتان أن النشر آمن للمستخدم الذي كان داخل التطبيق قبل دقائق أو ساعات.

التبويب القديم ليس نفايات ينبغي تجاهلها. إنه عميل حقيقي يشغّل إصدارًا حقيقيًا سابقًا.

عندما بدأت أنظر إلى النشر بهذه الطريقة، صار فشل الملفات المجزأة أقل غموضًا. بصمة المحتوى تعطي الملف هوية مستقرة. والكاش طويل المدة يجعل تلك الهوية فعالة. لكن على النشر أن يحترم هذه الهوية مدة كافية، أو أن يوفر للعميل طريقة مضبوطة للانتقال إلى النسخة الجديدة.

لا أحتاج إلى تشغيل كل إصدار قديم إلى الأبد. أحتاج فقط إلى أن يتحمل النظام الفترة التي يتعايش فيها العملاء القدامى والخوادم الجديدة بصورة طبيعية.

هذا هو عقد النشر الذي يهمني الآن: المستخدم الجديد يحصل على الإصدار الجديد، والمستخدم القديم لا يفقد الملفات التي ما زال إصداره يعرف كيف يطلبها، وأي اختلاف متبقٍ ينتهي بمسار استعادة مقصود بدل صفحة مكسورة.