الكتابات/tutorial/2026/06
Tutorial10 يونيو 2026·28 دقيقة

مكوّنات التخزين المؤقت في Next.js 16: الدليل الكامل لـ use cache و cacheLife و cacheTag

أتقن نموذج التخزين المؤقت الجديد في Next.js 16. يغطي هذا الدليل العملي توجيه use cache وملفات cacheLife والإبطال القائم على الوسوم عبر cacheTag و updateTag والعرض المسبق الجزئي والانتقال من التخزين المؤقت الضمني — مع أمثلة قابلة للتشغيل.

أعاد الإصدار Next.js 16 بهدوء كتابة أحد أكثر أجزاء الإطار إرباكًا: التخزين المؤقت. لسنوات، كان موجّه التطبيق (App Router) يخزّن البيانات بشكل عدواني وضمني — تُجمَّع طلبات الجلب وتُخزَّن دون أن تطلب ذلك، وتتحوّل مقاطع المسارات إلى ثابتة أو ديناميكية بناءً على قواعد لا يستطيع أحد سردها كاملةً، وأصبح سؤال "لماذا بياناتي قديمة؟" طقسًا من طقوس العبور.

مكوّنات التخزين المؤقت (Cache Components) تقلب هذا النموذج. أصبح التخزين المؤقت الآن اختياريًا بالكامل. افتراضيًا، تُعرَض كل صفحة بشكل ديناميكي وقت الطلب — وهو بالضبط ما يتوقّعه معظم المطوّرين من إطار عمل متكامل. وعندما تريد التخزين المؤقت، تستخدم توجيهًا صريحًا واحدًا: 'use cache'. يشرح هذا الدليل النموذج بالكامل من الصفر، مع أمثلة قابلة للتشغيل يمكنك وضعها في مشروع حقيقي.

المتطلبات الأساسية

قبل البدء، تأكّد من توفّر ما يلي:

  • Node.js 20.9 أو أحدث (أسقط Next.js 16 دعم Node 18)
  • TypeScript 5.1 أو أحدث إذا كنت تستخدم TypeScript
  • معرفة عملية بـ موجّه التطبيق (مكوّنات الخادم، التخطيطات، Suspense)
  • مشروع Next.js جديد أو قائم يمكنك ترقيته

يفترض هذا الدليل إلمامًا متوسّطًا بمكوّنات خادم React. لا تحتاج إلى خبرة سابقة بالعرض المسبق الجزئي — نبني هذا الفهم هنا.

ما الذي ستبنيه

سنبني مسار لوحة تحكّم صغيرة يجمع ثلاثة أنواع من المحتوى في صفحة واحدة:

  1. هيكل تسويقي ثابت يُعرَض فورًا من الذاكرة المؤقتة
  2. قائمة منتجات مخزّنة مؤقتًا تُحدَّث وفق جدول زمني
  3. قسم حيّ لكل طلب (إشعارات المستخدم المسجّل) يصل طازجًا عبر البث

في النهاية، ستفهم بدقّة أيّ أجزاء الصفحة مخزّنة مؤقتًا، ولأي مدّة، وكيف تُبطلها بدقّة — دون تخمين.

الخطوة 1: الترقية إلى Next.js 16

ابدأ بأداة التحويل الآلية. فهي تعالج معظم التغييرات الكاسرة، بما في ذلك تحويل params/searchParams إلى غير متزامنة وإعادة تسمية middleware.ts.

# الترقية الآلية (موصى بها)
npx @next/codemod@canary upgrade latest
 
# ...أو الترقية يدويًا
npm install next@latest react@latest react-dom@latest

بعض التغييرات الكاسرة مهمّة لهذا الدليل. أصبحت params وsearchParams وcookies() وheaders() وdraftMode() الآن غير متزامنة ويجب انتظارها بـ await:

// قبل (Next.js 15)
export default function Page({ params }) {
  const { id } = params
}
 
// بعد (Next.js 16)
export default async function Page({ params }) {
  const { id } = await params
}

أصبح Turbopack هو المُجمّع الافتراضي. إذا كان لديك إعداد webpack مخصّص، فعُد إليه عبر next build --webpack.

الخطوة 2: تفعيل مكوّنات التخزين المؤقت

مكوّنات التخزين المؤقت هي راية إعداد واحدة توحّد ما كان سابقًا ثلاث رايات تجريبية منفصلة (ppr وdynamicIO وuseCache).

// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

لحظة تفعيلها، يحدث أمران:

  • يعمل كل ما هو ديناميكي وقت الطلب افتراضيًا. لا مزيد من التخزين المؤقت العرضي.
  • يصبح العرض المسبق الجزئي (PPR) هو السلوك الافتراضي. اختفت راية experimental.ppr القديمة وتصدير المسار experimental_ppr — تحلّ مكوّنات التخزين المؤقت محلّهما بالكامل.

إذا كنت تستخدم سابقًا experimental.dynamicIO أو experimental.useCache، فأزل تلك الرايات؛ فإن cacheComponents هو خليفتها.

الخطوة 3: فهم الهيكل الثابت

إليك النموذج الذهني الذي يجعل كل شيء آخر واضحًا. عند تفعيل مكوّنات التخزين المؤقت، يعرض Next.js مسبقًا هيكل HTML ثابتًا لكل مسار ويقدّمه فورًا. وفي أي مكان تقرأ فيه شفرتك بيانات ديناميكية (الكوكيز، الترويسات، طلبات الجلب غير المخزّنة)، تغلّف ذلك الجزء داخل حدود <Suspense>. يبثّ Next.js المحتوى الديناميكي داخل الهيكل عندما يصبح جاهزًا.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Notifications } from './notifications'
 
export default function DashboardPage() {
  return (
    <main>
      {/* الهيكل الثابت — معروض مسبقًا، يُقدَّم فورًا */}
      <h1>لوحة التحكّم</h1>
      <p>مرحبًا بك في مركز التحكّم الخاص بك.</p>
 
      {/* ديناميكي — يُبَثّ لكل طلب */}
      <Suspense fallback={<p>جارٍ تحميل الإشعارات…</p>}>
        <Notifications />
      </Suspense>
    </main>
  )
}

يُعدّ <h1> و<p> جزءًا من الهيكل الثابت ويُحمَّلان فورًا في كل زيارة. أما <Notifications /> فيقرأ بيانات لكل طلب، لذا يعيش خلف حدود Suspense ويصل عبر البث. هذا هو العرض المسبق الجزئي عمليًا: مسار واحد، ثابت وديناميكي معًا، دون تنازل عن سرعة التحميل الأولي.

إذا قرأ مكوّنٌ بيانات ديناميكية دون حدود Suspense حوله، فسيُصدر Next.js خطأً وقت البناء ويخبرك بالضبط أين تضيفه. هذا الخطأ ميزة — فهو يجبرك على أن تقرّر صراحةً ما هو ثابت وما هو ديناميكي.

الخطوة 4: تخزين دالة مؤقتًا باستخدام use cache

والآن الميزة الرئيسية. أضف توجيه 'use cache' إلى دالة أو مكوّن أو ملف كامل لجعل مخرجاته قابلة للتخزين المؤقت. يفحص Next.js وسائط الدالة وقيم الإغلاق (closure) ليولّد مفتاح ذاكرة مؤقتة تلقائيًا — لن تكتب مفتاحًا يدويًا أبدًا.

// app/dashboard/products.tsx
import { cacheLife } from 'next/cache'
 
async function getProducts() {
  'use cache'
  cacheLife('hours')
 
  const res = await fetch('https://api.example.com/products')
  return res.json()
}
 
export async function ProductList() {
  const products = await getProducts()
 
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

لأن getProducts موسومة بـ 'use cache'، تُخزَّن نتيجتها وتُعاد عبر الطلبات وعبر النشر بأكمله — وليس لمستخدم واحد فقط. أول زائر يُسخّن الذاكرة المؤقتة، ومن بعده يقرأ الجميع القيمة المخزّنة حتى تنتهي صلاحيتها.

يمكنك أيضًا تخزين مكوّن بأكمله مؤقتًا بوضع التوجيه أعلى الدالة:

export async function ProductList() {
  'use cache'
  cacheLife('hours')
 
  const products = await getProducts()
  return <ul>{/* ... */}</ul>
}

أو تخزين كل تصدير في ملف مؤقتًا بوضع 'use cache' في أعلى الوحدة — مفيد لملف يحتوي على دوال جلب بيانات صرفة.

الخطوة 5: التحكّم في الطزاجة باستخدام cacheLife

كل قيمة مخزّنة مؤقتًا تحتاج إلى عمر. يتيح لك cacheLife اختيار ملف مُسمّى بدلاً من نثر أرقام سحرية عبر شفرتك. يأتي Next.js بملفّات مدمجة، ويرمّز كلٌّ منها وقت التقادم وميزانية إعادة التحقّق:

الملفاستخدمه لـ
'seconds'بيانات شبه فورية تتحمّل تقادمًا ضئيلًا
'minutes'محتوى متغيّر باستمرار (الخلاصات، الأسعار)
'hours'محتوى يتحدّث بضع مرّات في اليوم
'days'بيانات تسويقية أو كتالوج تُحدَّث يوميًا
'weeks'محتوى مرجعي نادر التغيّر
'max'دائم فعليًا حتى الإبطال الصريح
async function getHomepageBanner() {
  'use cache'
  cacheLife('days') // التحديث مرّة في اليوم تقريبًا
  return fetchBanner()
}

وللتحكّم الكامل، عرّف ملفًا مخصّصًا في إعدادك وأشِر إليه بالاسم:

// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    // نافذة طزاجة 5 دقائق، ثم إعادة التحقّق في الخلفية
    realtime: {
      stale: 60,      // التقديم من ذاكرة العميل لمدة 60 ثانية
      revalidate: 300, // التحديث على الخادم كل 5 دقائق
      expire: 3600,    // انتهاء صارم بعد ساعة
    },
  },
}
async function getMetrics() {
  'use cache'
  cacheLife('realtime')
  return fetchMetrics()
}

الخطوة 6: الوسم والإبطال باستخدام cacheTag

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

import { cacheLife, cacheTag } from 'next/cache'
 
async function getProduct(id: string) {
  'use cache'
  cacheLife('max')
  cacheTag(`product-${id}`, 'products')
 
  const res = await fetch(`https://api.example.com/products/${id}`)
  return res.json()
}

يحمل هذا الإدخال الآن وسمين: وسم محدّد product-123 ووسم عام products. يمكنك مسح منتج واحد أو الكتالوج بأكمله باستدعاء واحد. الإبطال القائم على الوسوم هو النموذج الصحيح لأي تطبيق ذي رسم بياني للبيانات غير بسيط — فهو يندمج مع العرض المسبق الجزئي بسلاسة ويمنحك تحكّمًا دقيقًا.

الخطوة 7: اختيار واجهة الإبطال الصحيحة

يأتي Next.js 16 بثلاث أدوات إبطال أوّلية، واختيار الصحيحة منها هو الفرق بين تطبيق سلس وآخر مربك. إليك القرار:

revalidateTag(tag, profile) — للاتساق النهائي

أصبح revalidateTag الآن يتطلّب ملف cacheLife كوسيطه الثاني، مفعّلًا نمط "قديم-أثناء-إعادة-التحقّق". يحصل المستخدمون على البيانات المخزّنة فورًا بينما يحدّثها Next.js في الخلفية. مثالي للمحتوى الذي يتحمّل تأخّرًا بثوانٍ.

import { revalidateTag } from 'next/cache'
 
// الافتراضي الموصى به للمحتوى طويل العمر
revalidateTag('products', 'max')
 
// أو نافذة مخصّصة مضمّنة
revalidateTag('products', { expire: 3600 })

صيغة الوسيط الواحد revalidateTag('products') مهملة — مرّر دائمًا ملفًا.

updateTag(tag) — لقراءة كتاباتك

عندما يرسل المستخدم نموذجًا ويجب أن يرى تغييره فورًا، استخدم updateTag داخل إجراء خادم (Server Action). فهو يُنهي صلاحية الذاكرة المؤقتة ويقرأ بيانات طازجة ضمن الطلب نفسه — دون ومضة محتوى قديم.

'use server'
 
import { updateTag } from 'next/cache'
 
export async function updateProduct(id: string, data: ProductData) {
  await db.products.update(id, data)
 
  // إنهاء الصلاحية + إعادة القراءة فورًا: يرى المستخدم تعديله على الفور
  updateTag(`product-${id}`)
}

refresh() — للبيانات غير المخزّنة

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

'use server'
 
import { refresh } from 'next/cache'
 
export async function markAsRead(notificationId: string) {
  await db.notifications.markAsRead(notificationId)
  refresh() // إعادة جلب عدّاد الإشعارات غير المخزّن في الترويسة
}

قاعدة بسيطة: updateTag عندما يغيّر المستخدم بيانات مخزّنة ويجب أن يراها الآن؛ revalidateTag عندما يكون التقادم مقبولًا للحظة؛ refresh عندما لم تكن البيانات مخزّنة أصلًا.

الخطوة 8: تجميع لوحة التحكّم الكاملة

اجمع الآن كل شيء في مسار واحد يوضّح الطبقات الثلاث — الهيكل الثابت، والقائمة المخزّنة، والمحتوى الحيّ لكل طلب.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
 
// الطبقة 2: قائمة منتجات مخزّنة، موسومة للإبطال الدقيق
async function ProductList() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')
 
  const res = await fetch('https://api.example.com/products')
  const products = await res.json()
 
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}
 
// الطبقة 3: حيّة لكل طلب — تقرأ الكوكيز، غير مخزّنة أبدًا
async function Notifications() {
  const res = await fetch('https://api.example.com/me/notifications', {
    headers: { /* مصادقة لكل مستخدم من الكوكيز */ },
  })
  const items = await res.json()
  return <span>{items.length} جديدة</span>
}
 
export default function DashboardPage() {
  return (
    <main>
      {/* الطبقة 1: هيكل ثابت — فوري */}
      <h1>لوحة التحكّم</h1>
 
      {/* الطبقة 2: مخزّنة، تُبَثّ من المخزن */}
      <section>
        <h2>المنتجات</h2>
        <Suspense fallback={<p>جارٍ تحميل المنتجات…</p>}>
          <ProductList />
        </Suspense>
      </section>
 
      {/* الطبقة 3: ديناميكية، تُبَثّ طازجة لكل طلب */}
      <section>
        <h2>الإشعارات</h2>
        <Suspense fallback={<p>جارٍ التحقّق…</p>}>
          <Notifications />
        </Suspense>
      </section>
    </main>
  )
}

مسار واحد، ثلاثة سلوكيات تخزين مؤقت، كلٌّ مختار صراحةً. تُعرَض الترويسة فورًا، وتُبَثّ المنتجات من الذاكرة المؤقتة، وتُبَثّ الإشعارات طازجة لكل مستخدم.

الخطوة 9: الانتقال من التخزين المؤقت الضمني

إذا كنت تُرقّي تطبيق موجّه تطبيق قائمًا، إليك كيف تُقابل الأنماط القديمة العالم الجديد.

النمط القديم (15)النهج الجديد (16)
fetch(..., { next: { revalidate: 3600 } })'use cache' + cacheLife('hours')
fetch(..., { next: { tags: ['x'] } })'use cache' + cacheTag('x')
export const revalidate = 60غلّف العمل المخزّن بـ 'use cache' + cacheLife
export const dynamic = 'force-static''use cache' على مستوى الصفحة
revalidateTag('x')revalidateTag('x', 'max') أو updateTag('x')
experimental.ppr / experimental_pprأُزيلت — cacheComponents: true

أكبر تحوّل ذهني: في الإصدار 15 كنت تختار الخروج من التخزين المؤقت؛ وفي 16 تختار الدخول إليه. ابدأ بكل شيء ديناميكيًا، ثم خزّن مؤقتًا بتعمّد حيث قِست فائدة.

اختبار تنفيذك

تحقّق من السلوك، ولا تفترضه:

  1. أكّد الهيكل الثابت. شغّل next build وافحص مخرجات البناء — يجب أن يُبلّغ مسارك عن هيكل معروض مسبقًا. وإذا أصدر Next.js خطأ حول حدود Suspense ناقصة، فأضِفها حيث تقرأ بيانات ديناميكية.
  2. راقب تسخين الذاكرة المؤقتة. في وضع التطوير، سجّل طابعًا زمنيًا داخل دالة 'use cache'. يُشغّلها الطلب الأول؛ وتعيد الطلبات اللاحقة ضمن نافذة cacheLife استخدام القيمة (يبقى الطابع الزمني مجمّدًا).
  3. اختبر الإبطال. شغّل إجراء الخادم وأكّد أن updateTag يُظهر بيانات طازجة في الطلب نفسه، بينما يقدّم revalidateTag القديم ثم الطازج.
  4. افحص البث. قلّل سرعة شبكتك في أدوات المطوّر — يجب أن ترى الهيكل يُرسَم أولًا، ثم تمتلئ الأقسام الديناميكية.

استكشاف الأخطاء وإصلاحها

خطأ بناء "تعذّر العرض المسبق للمسار". يقرأ مكوّن بيانات ديناميكية (كوكيز، ترويسات، أو جلب غير مخزّن) خارج حدود Suspense. غلّفه بـ <Suspense> أو خزّنه بـ 'use cache'.

بياناتي المخزّنة لا تتحدّث أبدًا. افحص ملف cacheLife وأكّد أنك تُبطل الوسم الصحيح. تذكّر أن revalidateTag يحتاج الآن إلى وسيط ملف — فصيغة الوسيط الواحد تفعل أقل ممّا تتوقّع بصمت.

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

params غير معرّفة. في Next.js 16، params وsearchParams وعود (Promises). انتظرها بـ await.

الخطوات التالية

الخلاصة

مكوّنات التخزين المؤقت هي أوضح قصّة تخزين مؤقت قدّمها Next.js على الإطلاق. بدلاً من تخمين ما قرّر الإطار تخزينه، تُصرّح به: 'use cache' للدخول، وcacheLife لضبط الطزاجة، وcacheTag للوسم بهدف الإبطال، والأداة الصحيحة من revalidateTag / updateTag / refresh للإبطال بدقّة. تُعرَض الصفحات ديناميكية افتراضيًا، وتُخزّن مؤقتًا بتعمّد، ويبثّ هيكل ثابت المحتوى الديناميكي للجمع بين أفضل العالمين. ابدأ بمسار واحد، وأثبت النموذج، ثم انشره من هناك.


المصادر: مدوّنة إصدار Next.js 16، مرجع إعداد cacheComponents.