إذا سبق لك التعامل مع التخزين المؤقت في Next.js 13 أو 14، فأنت تعرف ذلك الشعور المحبط — تُحدّث البيانات في قاعدة البيانات، تعود للتطبيق، وتجد نفس البيانات القديمة تحدّق فيك. النظام القديم كان يُخزّن كل شيء تلقائيًا: طلبات fetch، مسارات GET، حتى التنقل من جهة العميل. بصراحة، كان الأمر أشبه بصندوق أسود لا تعرف ماذا يحصل بداخله.
في Next.js 16، تغيّر الوضع تمامًا. الفكرة الآن بسيطة: لا يُخزَّن أي شيء إلا إذا طلبت ذلك صراحة — وهو ما يُسمّى «التخزين بالموافقة» (Cache by Consent). في هذا الدليل، سنستعرض نظام التخزين المؤقت الجديد من الألف إلى الياء: توجيه use cache، والتحكم بمدة التخزين عبر cacheLife، ووسم البيانات باستخدام cacheTag. كل شيء مع أمثلة عملية تقدر تطبقها مباشرة.
لماذا تغيّر نظام التخزين المؤقت في Next.js 16؟
قبل ما ندخل في التفاصيل، خلّنا نفهم كيف وصلنا لهنا. نظام التخزين المؤقت مرّ بثلاث مراحل رئيسية:
- Next.js 13-14: كل شيء يُخزَّن تلقائيًا. طلبات
fetchتستخدمforce-cacheافتراضيًا، معالجات GET تُخزَّن، والتنقل من العميل كذلك. المشكلة؟ المطورون ما كانوا يعرفون متى يرون بيانات مُخزّنة ومتى يرون بيانات حيّة. كان مصدر إحباط حقيقي. - Next.js 15: بدأ التحوّل — طلبات
fetchأصبحت تستخدمno-storeافتراضيًا، وأُلغي التخزين التلقائي للتنقل. لكن النظام ظل يعتمد علىunstable_cacheوهو — كما يوحي اسمه — لم يكن مستقرًا ولا مريحًا في الاستخدام. - Next.js 16: التحوّل الكامل مع نظام Cache Components وتوجيه
use cache. لا شيء يُخزَّن ما لم تُحدد ذلك بنفسك. أخيرًا!
الفكرة الجوهرية واضحة: بدلًا من أن يحاول الإطار تخمين ما يجب تخزينه (وغالبًا يُخطئ)، أصبح المطور هو المتحكم الكامل. وصراحة، هذا هو النهج الصحيح من البداية.
تفعيل Cache Components في المشروع
أول شيء تحتاج تسويه هو تفعيل ميزة Cache Components في ملف إعدادات Next.js. بدون هذا التفعيل، توجيه use cache ما راح يشتغل.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
بمجرد تفعيل cacheComponents، يتغيّر سلوك Next.js بشكل جذري. كل عمليات جلب البيانات تُستبعَد من التصيير المسبق (Pre-rendering) إلا إذا حددت صراحة أنها يجب أن تُخزَّن. يعني ببساطة: كل البيانات تُجلب في وقت التشغيل افتراضيًا، وأنت تختار ما يُخزَّن وما لا يُخزَّن.
توجيه use cache: أساس النظام الجديد
توجيه use cache هو قلب نظام التخزين المؤقت الجديد. فكّر فيه كنظير لـ use client أو use server — لكن بدلًا من تحديد بيئة التنفيذ، يقول للمُترجم: "خزّن ناتج هذا الكود مؤقتًا."
الجميل في الموضوع أن المُترجم (Compiler) يتولى المهمة الصعبة بدلًا عنك. يحلل الدالة، يفحص المُعطيات والمتغيرات المُلتقطة من النطاق الأعلى (Closures)، وينشئ مفتاح تخزين مؤقت فريد تلقائيًا. لا حاجة لتحديده يدويًا كما كان الحال مع unstable_cache.
على مستوى الملف
عند وضع التوجيه في أعلى الملف، تُخزَّن الصفحة بالكامل كصفحة ثابتة:
// app/blog/page.tsx
'use cache'
import { getBlogPosts } from '@/lib/data'
export default async function BlogPage() {
const posts = await getBlogPosts()
return (
<main>
<h1>المدونة</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</main>
)
}
على مستوى المكوّن
هذا الاستخدام ممتاز لما تريد تخزين مكوّن واحد فقط بينما يبقى باقي الصفحة ديناميكيًا:
// components/PopularProducts.tsx
import { db } from '@/lib/db'
export async function PopularProducts() {
'use cache'
const products = await db.product.findMany({
orderBy: { salesCount: 'desc' },
take: 10,
})
return (
<section>
<h2>المنتجات الأكثر مبيعًا</h2>
{products.map((product) => (
<div key={product.id}>
<h3>{product.name}</h3>
<span>${product.price}</span>
</div>
))}
</section>
)
}
على مستوى الدالة
وهذا هو الاستخدام الأكثر مرونة — تخزين نتيجة دالة جلب بيانات بشكل مستقل عن أي مكوّن. أنا شخصيًا أفضل هذا النمط لأنه يفصل منطق التخزين عن واجهة المستخدم:
// lib/data.ts
import { db } from '@/lib/db'
export async function getCategories() {
'use cache'
return await db.category.findMany({
orderBy: { name: 'asc' },
})
}
export async function getProductById(id: string) {
'use cache'
return await db.product.findUnique({
where: { id },
include: { reviews: true },
})
}
لاحظ شيء مهم هنا: في دالة getProductById، المُعطى id يصبح تلقائيًا جزءًا من مفتاح التخزين. يعني كل منتج يحصل على إدخال تخزين مؤقت منفصل — وكل هذا يحدث بدون أي إعداد إضافي من طرفك.
cacheLife: التحكم في مدة التخزين المؤقت
طيب، use cache حلو، لكن ماذا عن التحكم في مدة التخزين؟ في التطبيقات الحقيقية، ما تقدر تعامل كل البيانات بنفس الطريقة. أسعار المنتجات تحتاج تتحدث كل ساعة، بينما صفحة "من نحن" ممكن تظل مُخزّنة لأسابيع. هنا يأتي دور cacheLife.
ملفات التعريف المُدمجة
يوفر Next.js 16 عدة ملفات تعريف جاهزة تغطي أغلب الحالات:
seconds— تخزين لثوانٍ قليلة، للبيانات شبه الحيّةminutes— تخزين لدقائق، للبيانات المتغيّرة بشكل معتدلhours— تخزين لساعات، للبيانات شبه الثابتةdays— تخزين ليوم أو أكثر، لمحتوى المدونات والمقالاتweeks— تخزين لأسابيع، للمحتوى الثابت نادر التغييرmax— أطول مدة ممكنة، للمحتوى الذي ما يتغير أبدًا (تقريبًا)
import { cacheLife } from 'next/cache'
// تخزين أسعار المنتجات لساعة
export async function getProductPrices() {
'use cache'
cacheLife('hours')
return await db.product.findMany({
select: { id: true, price: true },
})
}
// تخزين مقالات المدونة ليوم كامل
export async function getBlogPosts() {
'use cache'
cacheLife('days')
return await db.post.findMany({
orderBy: { createdAt: 'desc' },
})
}
// تخزين صفحة "من نحن" لأسابيع
export async function getAboutContent() {
'use cache'
cacheLife('weeks')
return await db.page.findUnique({
where: { slug: 'about' },
})
}
إنشاء ملفات تعريف مخصصة
الملفات الجاهزة كويسة، لكن أحيانًا تحتاج تحكم أدقّ. يمكنك إنشاء ملفات تعريف مخصصة في إعدادات المشروع، وكل ملف تعريف يتحكم في ثلاث خصائص:
- stale: المدة التي يستخدم فيها العميل البيانات المُخزّنة دون التحقق من الخادم
- revalidate: بعد هذه المدة، يُعاد جلب البيانات في الخلفية عند الطلب التالي (نمط stale-while-revalidate)
- expire: الحد الأقصى لعمر التخزين — بعدها تُحذف البيانات المُخزّنة نهائيًا
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
experimental: {
cacheLife: {
// ملف تعريف للمحتوى التحريري
editorial: {
stale: 3600, // ساعة واحدة
revalidate: 14400, // 4 ساعات
expire: 604800, // أسبوع
},
// ملف تعريف لبيانات المنتجات
productData: {
stale: 300, // 5 دقائق
revalidate: 900, // 15 دقيقة
expire: 86400, // يوم واحد
},
// ملف تعريف للإعدادات شبه الثابتة
siteConfig: {
stale: 86400, // يوم واحد
revalidate: 259200, // 3 أيام
expire: 2592000, // 30 يومًا
},
},
},
}
export default nextConfig
ثم استخدم ملف التعريف المخصص في أي مكان:
import { cacheLife } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('productData')
return await db.product.findMany()
}
export async function getEditorialContent(slug: string) {
'use cache'
cacheLife('editorial')
return await db.article.findUnique({ where: { slug } })
}
cacheTag: وسم البيانات لإبطالها عند الحاجة
التخزين المؤقت بدون آلية إبطال فعّالة يُشبه (وأنا ما أبالغ هنا) وضع الطعام في الثلاجة بدون كتابة تاريخ الصلاحية عليه. ما تعرف متى تأكله ومتى ترميه. هنا يأتي دور cacheTag: يسمح لك بوسم أي إدخال مُخزّن بعلامة نصية تستخدمها لاحقًا لإبطال التخزين عند الحاجة.
وسم البيانات
import { cacheTag } from 'next/cache'
// وسم جميع المنتجات بعلامة عامة
export async function getProducts() {
'use cache'
cacheTag('products')
return await db.product.findMany()
}
// وسم منتج محدد بعلامة عامة وعلامة خاصة
export async function getProductById(id: string) {
'use cache'
cacheTag('products', `product-${id}`)
return await db.product.findUnique({ where: { id } })
}
// وسم التصنيفات
export async function getCategories() {
'use cache'
cacheTag('categories')
return await db.category.findMany()
}
لاحظ كيف وضعنا علامتين على دالة getProductById: علامة عامة products وعلامة خاصة product-${id}. هذا يعطيك مرونة كبيرة — تقدر تُبطل تخزين منتج واحد بعينه، أو تُبطل كل المنتجات دفعة واحدة. أنيق، صح؟
قواعد مهمة للوسوم
- الحد الأقصى 128 وسمًا لكل إدخال مُخزّن (أكثر من كافي لأي حالة واقعية)
- كل وسم يمكن أن يصل طوله إلى 256 حرفًا
- يجب استدعاء
cacheTagداخل دالة تحمل توجيهuse cache
إبطال التخزين المؤقت: revalidateTag مقابل updateTag
هنا تصير الأمور مثيرة فعلًا. اختيار الأداة الصحيحة للإبطال يؤثر مباشرة على تجربة المستخدم، ويوفر Next.js 16 طريقتين مختلفتين لكل منها استخدام محدد.
revalidateTag — إعادة التحقق في الخلفية
تُستخدم لما يكون تأخر التحديث لبضع ثوانٍ مقبولًا. تعمل بنمط stale-while-revalidate — يعني تُقدّم البيانات القديمة فورًا للمستخدم الحالي، بينما تُجلب البيانات الجديدة في الخلفية للطلب التالي:
'use server'
import { revalidateTag } from 'next/cache'
// في Server Action أو Route Handler
export async function publishArticle(data: FormData) {
await db.article.create({ data: { /* ... */ } })
// إعادة التحقق باستخدام stale-while-revalidate
revalidateTag('articles', 'max')
}
المعامل الثاني max يُحدد ملف تعريف إعادة التحقق. تقدر أيضًا تستخدم ملفات التعريف الأخرى مثل hours أو days، أو تمرير كائن مخصص:
// إعادة تحقق مخصصة: جلب جديد خلال ساعة
revalidateTag('products', { revalidate: 3600 })
updateTag — تحديث فوري
هذه تُستخدم حصريًا في Server Actions لما يحتاج المستخدم يشوف تغييراته فورًا. تخيّل مثلًا أن المستخدم حدّث اسمه في الملف الشخصي — ما يعقل ترجعه لنفس الصفحة ويشوف الاسم القديم!
'use server'
import { updateTag } from 'next/cache'
export async function updateProfile(data: FormData) {
const name = data.get('name') as string
await db.user.update({
where: { id: currentUser.id },
data: { name },
})
// إبطال فوري — المستخدم سيرى تغييراته مباشرة
updateTag('user-profile')
}
متى تستخدم كل واحدة؟
القاعدة بسيطة:
- revalidateTag: للمحتوى العام — قوائم المنتجات، المقالات، التصنيفات. لو شاف المستخدم بيانات قديمة لبضع ثوانٍ، ما في مشكلة.
- updateTag: لبيانات المستخدم الشخصية — الملف الشخصي، الإعدادات، سلة المشتريات. المستخدم يتوقع يشوف تغييراته فورًا.
أنماط عملية: دمج المحتوى المُخزّن والديناميكي
من أقوى مزايا النظام الجديد هو إمكانية مزج المحتوى المُخزّن مع الديناميكي في نفس الصفحة. هذا شيء ما كان سهل تحقيقه في الإصدارات السابقة — كانت الصفحة إما مُخزّنة بالكامل أو ديناميكية بالكامل. الآن تقدر تختار بدقة.
نمط القشرة الثابتة مع محتوى ديناميكي
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { CachedSidebar } from '@/components/CachedSidebar'
import { LiveNotifications } from '@/components/LiveNotifications'
import { UserStats } from '@/components/UserStats'
export default function DashboardPage() {
return (
<div className="dashboard">
{/* الشريط الجانبي مُخزّن مؤقتًا — لا يتغير كثيرًا */}
<CachedSidebar />
<main>
{/* الإشعارات ديناميكية — تُجلب في كل طلب */}
<Suspense fallback={<p>جارٍ تحميل الإشعارات...</p>}>
<LiveNotifications />
</Suspense>
{/* إحصائيات المستخدم مُخزّنة لدقائق */}
<Suspense fallback={<p>جارٍ تحميل الإحصائيات...</p>}>
<UserStats />
</Suspense>
</main>
</div>
)
}
// components/CachedSidebar.tsx
import { cacheLife, cacheTag } from 'next/cache'
export async function CachedSidebar() {
'use cache'
cacheLife('days')
cacheTag('sidebar')
const menuItems = await db.menu.findMany({
orderBy: { sortOrder: 'asc' },
})
return (
<aside>
<nav>
{menuItems.map((item) => (
<a key={item.id} href={item.url}>{item.label}</a>
))}
</nav>
</aside>
)
}
// components/UserStats.tsx
import { cacheLife, cacheTag } from 'next/cache'
export async function UserStats() {
'use cache'
cacheLife('minutes')
cacheTag('user-stats')
const stats = await db.analytics.aggregate({
_count: { visits: true },
_sum: { revenue: true },
})
return (
<div className="stats">
<div>الزيارات: {stats._count.visits}</div>
<div>الإيرادات: ${stats._sum.revenue}</div>
</div>
)
}
نمط صفحة المنتج مع تخزين متعدد المستويات
هذا النمط يوضح كيف صفحة واحدة تقدر تحتوي على طبقات تخزين مختلفة — وهو شيء تحتاجه في أي متجر إلكتروني جدّي:
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { getProductById } from '@/lib/data'
import { ProductReviews } from '@/components/ProductReviews'
import { RelatedProducts } from '@/components/RelatedProducts'
import { StockStatus } from '@/components/StockStatus'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// بيانات المنتج مُخزّنة لساعات (نادرًا ما تتغير)
const product = await getProductById(id)
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>${product.price}</span>
{/* حالة المخزون ديناميكية — تُجلب في كل طلب */}
<Suspense fallback={<p>جارٍ التحقق من التوفر...</p>}>
<StockStatus productId={id} />
</Suspense>
{/* المراجعات مُخزّنة لدقائق */}
<Suspense fallback={<p>جارٍ تحميل المراجعات...</p>}>
<ProductReviews productId={id} />
</Suspense>
{/* المنتجات ذات الصلة مُخزّنة ليوم كامل */}
<Suspense fallback={<p>جارٍ تحميل المنتجات ذات الصلة...</p>}>
<RelatedProducts categoryId={product.categoryId} />
</Suspense>
</article>
)
}
الترقية من unstable_cache إلى use cache
إذا مشروعك يستخدم unstable_cache، الخبر السار أن الترقية تُبسّط الكود بشكل واضح. دعنا نشوف الفرق:
الطريقة القديمة
import { unstable_cache } from 'next/cache'
// كنت تحتاج لتحديد مفتاح التخزين يدويًا
const getCachedProducts = unstable_cache(
async (categoryId: string) => {
return await db.product.findMany({
where: { categoryId },
})
},
['products'], // مفتاح التخزين — يدوي
{
tags: ['products'], // الوسوم — يدوية
revalidate: 3600, // مدة إعادة التحقق — يدوية
}
)
الطريقة الجديدة
import { cacheLife, cacheTag } from 'next/cache'
// أبسط وأوضح — المُترجم يُنشئ المفتاح تلقائيًا
export async function getProducts(categoryId: string) {
'use cache'
cacheLife('hours')
cacheTag('products')
return await db.product.findMany({
where: { categoryId },
})
}
شوف الفرق! الكود أقصر، أوضح، وأقل عرضة للأخطاء. الفروقات الرئيسية:
- مفتاح التخزين: في النظام القديم كنت تحدده يدويًا كمصفوفة نصوص (ومع كل تعديل تخاف تنسى تحدّثه). في الجديد، المُترجم ينشئه تلقائيًا من المُعطيات والنطاق.
- أنواع البيانات: القديم يدعم JSON فقط. الجديد يدعم أي شيء يمكن لـ React Server Components تسلسله — بما فيها JSX نفسه.
- الأسلوب: القديم دالة غلاف (Wrapper) تلف الكود. الجديد توجيه لغوي (Directive) أنيق مثل
use server.
تحذيرات مهمة وأخطاء شائعة
قبل ما تبدأ تطبق النظام الجديد في مشروعك، في بعض الأشياء اللي لازم تعرفها — تعلمتها بالطريقة الصعبة حتى ما تضطر أنت:
لا يمكنك استخدام واجهات وقت التشغيل داخل use cache
الدوال المُخزّنة ما تقدر تستدعي cookies() أو headers() أو تقرأ searchParams مباشرة. وهذا منطقي لو فكرت فيه — كيف تُخزّن نتيجة تعتمد على بيانات تتغير مع كل طلب؟ الحل بسيط: اقرأ هذه القيم خارج النطاق المُخزّن ومرّرها كمُعطيات:
// خطأ — لا يعمل
export async function getUserData() {
'use cache'
const token = cookies().get('token') // خطأ!
return await fetchUser(token)
}
// صحيح — مرّر القيمة كمُعطى
export async function getUserData(token: string) {
'use cache'
return await fetchUser(token)
}
// في الصفحة
import { cookies } from 'next/headers'
export default async function Page() {
const token = (await cookies()).get('token')?.value ?? ''
const user = await getUserData(token)
return <div>{user.name}</div>
}
لف المحتوى الديناميكي بـ Suspense
عند تفعيل cacheComponents، أي مكوّن يجلب بيانات بدون use cache يُعتبر ديناميكيًا ولازم يكون ملفوف بـ Suspense. إذا نسيت هذا، ستحصل على خطأ واضح أثناء البناء:
// خطأ: Uncached data was accessed outside of Suspense
export default async function Page() {
const data = await fetchDynamicData() // لا يوجد use cache
return <div>{data}</div>
}
// صحيح: لف المكوّن الديناميكي بـ Suspense
export default function Page() {
return (
<Suspense fallback={<p>جارٍ التحميل...</p>}>
<DynamicContent />
</Suspense>
)
}
سلوك التداخل في use cache
هذه نقطة دقيقة: عند تداخل عدة دوال تحمل use cache، كل واحدة تستخدم ملف التعريف الخاص بها. لكن — وهنا المهم — الدالة الخارجية تُخزّن الناتج الكامل بما فيه نواتج الدوال الداخلية. فلما ينتهي تخزين الدالة الخارجية، تُعاد جلب كل البيانات من جديد، حتى لو التداخلية ما انتهت صلاحيتها بعد.
use cache: remote و use cache: private
إضافة إلى التوجيه الأساسي، يوفر Next.js 16 نوعين إضافيين يستحقان الذكر:
- use cache: remote — يسمح للمنصات (مثل Vercel) بتوفير مُعالج تخزين مؤقت مُخصص كـ Redis أو KV Database. مفيد جدًا لما يكون عندك عدة خوادم وتحتاج تخزين مُستمر ومُشترك بينها. طبعًا يتطلب اتصالًا شبكيًا، لكن الفائدة تستاهل.
- use cache: private — يسمح بالوصول لواجهات وقت التشغيل مثل
cookies()وheaders()داخل النطاق المُخزّن. لكن خلّي بالك: النتائج تُخزّن فقط في ذاكرة المتصفح ولا تستمر عبر إعادة تحميل الصفحة.
مثال تطبيقي شامل: متجر إلكتروني
خلّنا نجمع كل شيء في مثال واقعي. هذا متجر إلكتروني يستخدم طبقات تخزين مختلفة حسب طبيعة كل نوع من البيانات:
// lib/store-data.ts
import { cacheLife, cacheTag } from 'next/cache'
import { db } from '@/lib/db'
// تصنيفات المتجر — تتغير نادرًا جدًا
export async function getStoreCategories() {
'use cache'
cacheLife('weeks')
cacheTag('store-categories')
return await db.category.findMany({
include: { _count: { select: { products: true } } },
orderBy: { sortOrder: 'asc' },
})
}
// منتجات التصنيف — تتغير بشكل معتدل
export async function getCategoryProducts(categorySlug: string) {
'use cache'
cacheLife('hours')
cacheTag('products', `category-${categorySlug}`)
return await db.product.findMany({
where: { category: { slug: categorySlug } },
orderBy: { createdAt: 'desc' },
})
}
// تفاصيل منتج واحد
export async function getProduct(slug: string) {
'use cache'
cacheLife('hours')
cacheTag('products', `product-${slug}`)
return await db.product.findUnique({
where: { slug },
include: { category: true, images: true },
})
}
// مراجعات المنتج — تتغير بشكل متكرر
export async function getProductReviews(productId: string) {
'use cache'
cacheLife('minutes')
cacheTag(`reviews-${productId}`)
return await db.review.findMany({
where: { productId },
orderBy: { createdAt: 'desc' },
take: 20,
})
}
// app/actions.ts
'use server'
import { revalidateTag, updateTag } from 'next/cache'
import { db } from '@/lib/db'
// إضافة مراجعة جديدة
export async function addReview(productId: string, data: FormData) {
const content = data.get('content') as string
const rating = Number(data.get('rating'))
await db.review.create({
data: { productId, content, rating },
})
// تحديث فوري — المستخدم يرى مراجعته مباشرة
updateTag(`reviews-${productId}`)
}
// تحديث سعر منتج (من لوحة الإدارة)
export async function updateProductPrice(slug: string, newPrice: number) {
await db.product.update({
where: { slug },
data: { price: newPrice },
})
// إعادة تحقق في الخلفية — مقبول لتحديثات الإدارة
revalidateTag(`product-${slug}`, 'max')
revalidateTag('products', 'max')
}
الأسئلة الشائعة
هل يمكنني استخدام use cache مع مكونات العميل؟
لا. توجيه use cache يعمل فقط مع مكونات الخادم والدوال التي تعمل على الخادم. مكونات العميل (اللي تحمل use client) ما تقدر تستخدمه. لكن الحل سهل: خزّن البيانات في مكوّن خادم ومرّرها لمكوّن العميل عبر الخصائص (Props).
ما الفرق بين use cache و ISR التقليدي؟
ISR التقليدي يعمل على مستوى الصفحة بالكامل — كلها أو لا شيء. أما use cache فيعمل على مستوى أدقّ بكثير: تقدر تخزّن دالة واحدة أو مكوّن واحد داخل صفحة ديناميكية. مثلًا، شريط التنقل مُخزّن لأسابيع بينما الإشعارات تُجلب حيّة في كل طلب. هذه المرونة ما كانت ممكنة مع ISR.
هل أحتاج لـ dynamicIO مع cacheComponents؟
إذا كنت على Next.js 16، لا. cacheComponents هو الخيار المُوصى به ويحلّ محل dynamicIO التجريبي من Next.js 15. ابدأ بـ cacheComponents: true وما تشيل همّ.
كيف أختبر التخزين المؤقت محليًا؟
نقطة مهمة — التخزين المؤقت ما يشتغل في وضع التطوير (next dev) بنفس طريقة الإنتاج. لاختبار السلوك الفعلي، نفّذ next build ثم next start. هذا يشغّل خادم الإنتاج محليًا ويسمح لك بالتحقق من أن التخزين والإبطال يعملان كما تتوقع.
هل يدعم use cache بيئة Edge Runtime؟
نعم، ويشتغل بسلاسة. تقدر تستخدم use cache: remote مع مُعالج مُخصص (Redis أو Vercel KV مثلًا) لضمان تخزين مُستمر عبر عدة مناطق جغرافية. هذا مفيد جدًا للتطبيقات العالمية.