تخطَّ إلى المحتوى

التخزين المؤقّت في Next.js: الطبقات الأربع وأيّها أوقعك

حفظ الطلب في الذاكرة وData Cache وFull Route Cache وRouter Cache أربع آليات منفصلة بأربعة أعمار منفصلة. معظم أخطاء التخزين المؤقّت خلط بينها.

6 دقيقة قراءة

تكاد كل شكوى عن التخزين المؤقّت تصلنا تنتهي إلى الجملة نفسها: «غيّرنا البيانات ولم تتحدّث الصفحة». وهذا وصف معقول للعَرَض ووصف عديم الفائدة للسبب، لأن في Next.js أربع ذاكرات مؤقّتة، والعلاج مختلف في كلٍّ منها.

يستحقّ الأمر الفصل بينها، فلكلٍّ مفاتيحها وأعمارها وطرق إبطالها.

الطبقات الأربع

الطبقةتعيشالنطاقيمسحها
حفظ الطلب في الذاكرةمرور عرض واحدالخادملا شيء - تنتهي وحدها
Data Cacheعبر الطلبات وعمليات النشرالخادمrevalidateTag وrevalidatePath والوقت
Full Route Cacheحتى إعادة التحقّق أو النشرالخادمالاستدعاءات نفسها، وبناء جديد
Router Cacheثوانٍ إلى دقائقمتصفّح زائر واحدالتنقّل، router.refresh()

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

حفظ الطلب في الذاكرة

داخل عرض واحد، يُنفَّذ استدعاءان متطابقان لـfetch بالرابط والخيارات نفسها مرّة واحدة. هذه من React لا من Next.js، وهي موجودة كي يستطيع التخطيط والصفحة كلاهما طلب المستخدم الحالي دون تمريره عبر الخصائص.

// كلاهما يعمل؛ طلب واحد فقط يغادر الخادم.
const user = await fetch('https://api.example.com/me').then((r) => r.json());

نطاقها مرور عرض واحد. لا يمكن أن تَقدُم، ولا يمكن مسحها، ونادرًا جدًا أن تكون الطبقة التي سبّبت خللك. معرفتها مفيدة أساسًا كي تتوقّف عن كتابة شيفرة تخزين تكرّر ما تفعله.

الـ Data Cache

هذه هي التي يستحقّ فهمها جيدًا. تخزّن نتيجة fetch على الخادم، وتبقى عبر الطلبات، وتبقى عبر عمليات النشر.

النقطة الأخيرة تفاجئ الكثيرين. إعادة النشر لا تُفرغ الـ Data Cache.

// مخزَّن حتى يُبطل شيءٌ ما الوسم.
const posts = await fetch('https://cms.example.com/posts', {
  next: { tags: ['posts'] },
});
 
// مخزَّن 60 ثانية كحدّ أقصى.
const rates = await fetch('https://api.example.com/rates', {
  next: { revalidate: 60 },
});
 
// لا يُخزَّن أبدًا.
const cart = await fetch('https://api.example.com/cart', {
  cache: 'no-store',
});

الوسوم هي الآلية التي تجعل موقع محتوى قابلًا للإدارة. ضع وسمًا على الطلب، ودع خطّاف الـ CMS يمسحه:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
 
export async function POST(request: Request) {
  const secret = request.headers.get('x-webhook-secret');
  if (secret !== process.env.REVALIDATE_SECRET) {
    return new Response('Unauthorised', { status: 401 });
  }
 
  revalidateTag('posts');
  return Response.json({ revalidated: true });
}

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

الـ Full Route Cache

حين لا تكون للمسار مدخلات ديناميكية، يعرضه Next.js وقت البناء ويقدّم الـ HTML وحمولة RSC المخزَّنَين. هذه هي الذاكرة التي يقصدها الناس حين يقولون «ثابت».

المثير هو كم يسهل خروج مسارٍ منها. قراءة cookies() أو headers() أو searchParams، أو استدعاء fetch بـcache: 'no-store'، تنقل المسار كلّه إلى العرض الديناميكي. يكفي مساعد صغير في عمق مكوّن مشترك، والإشارة الوحيدة الظاهرة هي مخرجات البناء:

Route (app)
┌ ○ /                     static
├ ƒ /products             dynamic     <- كان ثابتًا الأسبوع الماضي
└ ○ /about                static

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

الـ Router Cache

هذه تعيش في متصفّح الزائر، وهي سبب رؤية زميلك بيانات قديمة بعد أن أخبرته أن المشكلة حُلّت.

حين ينتقل الزائر من جهة العميل، تُحفَظ حمولة RSC للوجهة في الذاكرة لنافذة قصيرة. اخرج وعُد ضمن تلك النافذة ولن يُعاد جلب شيء. التحديث الكامل يمسحها، والانتقال اللطيف لا يمسحها.

بعد أي تعديل، امسحها صراحةً:

'use client';
 
import { useRouter } from 'next/navigation';
 
export function DeleteButton({ id }: { id: string }) {
  const router = useRouter();
 
  async function remove() {
    await fetch(`/api/items/${id}`, { method: 'DELETE' });
    router.refresh(); // تجاهل ذاكرة العميل وأعد العرض على الخادم.
  }
 
  return <button onClick={remove}>حذف</button>;
}

داخل Server Action، تقوم revalidatePath بالنصفين معًا - تمسح ذاكرة الخادم وتُعلّم ذاكرة العميل قديمة - ولهذا تنتمي التعديلات إلى الـ actions لا إلى معالِجات مسارات مكتوبة يدويًا كلّما كان الخيار بيدك.

كيف نشخّص هذا عمليًا

السؤال دائمًا «أيّ طبقة»، وله جواب في ثلاث خطوات.

  1. هل هو قديم للجميع أم لشخص واحد؟ لشخص واحد: إنه الـ Router Cache. للجميع: المشكلة على الخادم.
  2. هل تحلّه إعادة النشر؟ إن نعم، فهو الـ Full Route Cache. وإن لا، فهو الـ Data Cache الذي - مرّة أخرى - يبقى عبر عمليات النشر.
  3. هل يظهر المسار بعلامة ƒ في مخرجات البناء؟ إن ظهر وكنت تتوقّع ، فأنت لا تشخّص ذاكرة مؤقّتة أصلًا، بل عرضًا ديناميكيًا عَرَضيًا، وهي مشكلة أخرى وأغلى عادةً.

معظم الفرق التي نعمل معها لا تحتاج استراتيجية تخزين أكثر شراسة. تحتاج أن تعرف مع أيٍّ من الأربع تتجادل الآن، وعُرفًا لتسمية الوسوم تلتزم به فعلًا.

هذا، والنموذج الذي تحته: نموذج العرض في App Router، مشروحًا كما ينبغي.

العودة إلى كل المقالات