حين ينمو مشروع Next.js ويتجاوز طاقة خادمٍ واحد، تنتقل إلى تشغيله على عدة خوادم (Multi-Node) خلف موازِن حِمل. لكن هنا تصطدم بمشكلةٍ خفيّة لا تظهر إلا في الإنتاج: كل نسخة تحتفظ بكاشها الخاص. فصفحة وُلِّدت بـISR على النسخة الأولى تبقى قديمة على الثانية، وإبطال الكاش يصيب نسخة واحدة فقط. النتيجة أنّ الزائر يرى محتوًى مختلفًا في كل طلب حسب النسخة التي خدمته. هذا الدليل المتقدّم — وهو Next.js Multi-Node نادر المحتوى عربيًا — يشرح المشكلة بدقّة ثم يبني الحلّ: كاش مشترك عبر Redis وموازِن حِمل صحيح، مع إثباتٍ فعليٍّ قِسناه على خادم مرام.

محتويات الدليل
لماذا يختلف الكاش على كل نسخة في Next.js Multi-Node؟
افتراضيًا، يخزّن Next.js نتائج التوليد التدريجي (ISR) وكاش البيانات (Data Cache) على نظام الملفات المحلي داخل مجلد .next/cache الخاص بكل عملية. هذا ممتاز على خادمٍ واحد، لكنه يصبح مشكلة في نشر Next.js Multi-Node؛ فحين تشغّل نسختين أو أكثر (على خوادم مختلفة أو حتى عمليات منفصلة)، تملك كلٌّ منها مجلد كاش معزولًا:
- توليد غير متزامن: صفحة ISR تُعاد بناؤها على النسخة أ لا تُحدَّث تلقائيًا على النسخة ب.
- إبطال جزئي: نداء
revalidatePathيصل النسخة التي وجّهها إليها الموازِن فقط، والبقية تبقى قديمة. - محتوى متذبذب: الزائر قد يرى النسخة الجديدة ثم القديمة في طلبين متتاليين حسب النسخة التي خدمته.
- تحسين الصور المكرّر: كل نسخة تعيد تحسين الصور نفسها وتخزّنها محليًا — هدر للموارد.
ملاحظة: المشكلة لا تظهر في التطوير ولا على خادمٍ واحد، بل فقط بعد التوسّع الأفقي — ولهذا يفاجأ بها كثير من الفرق في الإنتاج. الحلّ الجذري: إخراج الكاش من قرص كل نسخة إلى مخزنٍ مشترك.
حلّ Next.js Multi-Node: Cache Handler مخصّص إلى Redis
يتيح Next.js منذ الإصدار 13.5 تعريف Cache Handler مخصّص يتحكّم بمكان تخزين الكاش التدريجي. بربطه بمخزنٍ مشترك مثل Redis، تقرأ كل النسخ وتكتب من مصدرٍ واحد — فيتطابق المحتوى، ويصل إبطال الكاش إلى الجميع فورًا. هذه هي البنية الصحيحة لأي نشر Next.js Multi-Node:

الخطوة 1: ربط Cache Handler بـRedis
ثبّت حزمة معالج الكاش وعميل Redis، ثم عرّف المعالج في next.config.js. نستخدم هنا @neshca/cache-handler مع محوّل الـstrings الذي يعمل مع أي Redis عادي:
npm i @neshca/cache-handler redis
// cache-handler.js
const { CacheHandler } = require('@neshca/cache-handler');
const createRedisHandler = require('@neshca/cache-handler/redis-strings').default;
const createLruHandler = require('@neshca/cache-handler/local-lru').default;
const { createClient } = require('redis');
CacheHandler.onCreation(async () => {
const client = createClient({ url: 'redis://localhost:6379' });
client.on('error', () => {});
await client.connect();
const redis = await createRedisHandler({
client,
keyPrefix: 'nextcache:',
timeoutMs: 2000,
});
// احتياطي محلي في الذاكرة إن تعذّر Redis
const lru = createLruHandler();
return { handlers: [redis, lru] };
});
module.exports = CacheHandler;
// next.config.js
const nextConfig = {
output: 'standalone',
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // عطّل الكاش الافتراضي في الذاكرة لإجبار المشترك
};
module.exports = nextConfig;
💡 نصيحة: cacheMaxMemorySize: 0 مهمّ: يُعطّل الكاش الافتراضي في ذاكرة كل عملية، فتُجبر كل النسخ على المرور بـRedis — وإلا بقي جزء من الكاش محليًا ومتباينًا.
الخطوة 2: تشغيل النسخ وموازِن الحِمل
ابنِ التطبيق مرّة واحدة، وانشر نفس الإصدار على كل النسخ (لتطابق مُعرّفات البناء)، ثم شغّل عدة عمليات بـPM2 على منافذ مختلفة:
npm run build
# نسختان على منفذين مختلفين
PORT=3001 pm2 start .next/standalone/server.js --name web1
PORT=3002 pm2 start .next/standalone/server.js --name web2
pm2 save
ثم يوزّع Nginx الطلبات بينها عبر كتلة upstream (موازِن حِمل):
# /etc/nginx/nginx.conf (أو موقع منفصل)
upstream nextcluster {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
# keepalive 32; # لأداء أفضل
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://nextcluster;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

ملاحظة: لأنّ الكاش صار مشتركًا في Redis، لم تعد بحاجة إلى «التصاق الجلسة» (sticky sessions) لأجل الكاش؛ أي نسخة تخدم أي طلب بنفس المحتوى. توزيع round-robin البسيط يكفي.
إثبات فعلي من خادم مرام
لا نكتفي بالنظرية. نشرنا هذا الإعداد فعليًا على خادم مرام: تطبيق Next.js 14.2، نسختان بـPM2 على منفذين، وNginx موازِن حِمل أمامهما، وRedis محلي. ثم فحصنا مفاتيح Redis في الحالتين:

🤝 من واقع مرام: النتيجة القاطعة: قبل ربط معالج الكاش كان مخزن Redis فارغًا تمامًا (صفر مفاتيح) — أي أنّ كل نسخة تخزّن كاشها في .next/cache المحلي المعزول. بعد ربط @neshca/cache-handler بـRedis، ظهرت مفاتيح الكاش المشتركة فعليًا: nextcache:/cache وnextcache:__sharedTags__ وnextcache:__revalidated_tags__. أي أنّ كاش الصفحة انتقل من قرص كل نسخة إلى Redis واحد تقرأه كل النسخ. هذه المفاتيح مأخوذة حرفيًا من الخادم بعد التفعيل — دليلٌ ملموس أنّ المشكلة حُلّت من جذرها.
نقاط مهمّة قبل الإنتاج
- نفس البناء على كل النسخ: انشر نفس ناتج
build(نفسBUILD_ID) على جميع الخوادم، وإلا اختلفت مفاتيح الكاش. - Redis منفصل وموثوق: في نشرٍ متعدّد الخوادم، ضع Redis على خادمٍ يصل إليه الجميع، وفعّل الثبات (persistence) والمصادقة.
- احتياطي محلي: أبقِ معالجًا محليًا (LRU) بعد Redis في القائمة، كي يستمر الموقع إن تعذّر Redis لحظيًا.
- الأصول الثابتة: اخدم
/_next/staticمن Nginx بكاش دائم؛ فهي تحمل بصمة فريدة ولا تحتاج مشاركة. - راقب Redis: تابع استهلاك الذاكرة وعدد المفاتيح، واضبط سياسة الإخلاء (eviction) المناسبة.
الخلاصة
تشغيل Next.js Multi-Node ليس مجرّد نسخ التطبيق على عدة خوادم خلف موازِن حِمل؛ فبدون كاشٍ مشترك ستواجه محتوًى متذبذبًا وإبطالًا جزئيًا يربك مستخدميك. الحلّ الصحيح هو ربط Cache Handler مخصّص بـRedis، فيتحوّل الكاش من جزرٍ معزولة على كل نسخة إلى مصدرِ حقيقةٍ واحد يشاركه الجميع — وقد أثبتنا فعليًا أنّ كاش الصفحة ينتقل إلى Redis بمجرّد التفعيل. أتقِن هذه القطعة، وسيتوسّع مشروعك أفقيًا بثقة، بمحتوًى متطابق وأداءٍ متّسق مهما زادت الخوادم.
لإكمال الصورة: راجع تشغيل Next.js على VPS بدل Vercel (الأساس)، واستضافة Node.js بطريقة Production (PM2 والعنقود)، ومتى تبدأ التوسّع الأفقي؟. وللتوثيق الرسمي راجع دليل Next.js حول الكاش وISR، وتوثيق Redis الرسمي، وحزمة @neshca/cache-handler، ووحدة upstream في Nginx.
تبني نشرًا موزّعًا لتطبيق Next.js؟ خوادم مرام تمنحك موارد مخصّصة وRedis وشبكة داخلية سريعة — الأساس الصحيح للتوسّع الأفقي بثقة.
اطلب خادم VPS للتوسّع
