أطر العمل
العرض والتخزين المؤقّت في Next.js بعمق
دليل خبير لكيفية عرض موجّه تطبيق Next.js وتخزينه مؤقّتًا — العرض الساكن مقابل الديناميكيّ وما الذي يُطلِق كلًّا، وطبقات التخزين الأربع (تحفيظ الطلب، وذاكرة البيانات، وذاكرة المسار الكاملة، وذاكرة الموجّه)، وتخزين fetch في Next 15+ (غير مخزّن افتراضيًّا)، وإعادة التحقّق الزمنية وعند الطلب بـ revalidateTag/revalidatePath، وunstable_cache للبيانات غير fetch، وتهيئة مقطع المسار، والتدفّق بـ Suspense وloading.tsx، والعرض المسبق الجزئيّ — مع أخطاء شائعة وتمارين.
أصعب ما يُعقَل التفكير فيه في موجّه تطبيق Next.js هو متى تُعرَض الأشياء وما الذي يُخزَّن. وبمجرّد أن تستطيع التنبّؤ بذلك — هل هذه الصفحة ساكنة أم ديناميكية؟ هل هذا fetch مخزّن؟ كيف أُبطِله؟ — يصير الإطار حتميًّا بدل غامض. هذه التدوينة ذلك النموذج، بعمق الخبير. تفترض أساسيات Next.js (مكوّنات الخادم، وموجّه app/) ونموذج جلب البيانات على العميل الذي تقابله. كل شيءٍ بـ TypeScript، مستهدفةً موجّه التطبيق الحديث (Next.js 15+).
النموذج الذي تمسكه: لدى Next.js نمطا عرضٍ (ساكن، يُنجَز مسبقًا؛ وديناميكيّ، يُنجَز لكل طلب) وأربع ذواكر مستقلّة تجلس بين كودك والمستخدم. كل سؤال أداءٍ هو حقًّا "في أيّ نمط عرضٍ هذا المسار، وأيّ ذاكرةٍ تخدم هذه البيانات؟" تعلّم الإجابة عن هذين، فيتوقّف التخزين عن كونه تخمينًا.
العرض الساكن مقابل الديناميكيّ
يُعرَض مقطع المسار بإحدى طريقتين:
- ساكن (معروض مسبقًا) — يُعرَض وقت البناء (أو في الخلفية عند إعادة التحقّق) إلى HTML ويُخزَّن. يحصل المستخدم على ملفٍّ فورًا. وهذا الافتراضيّ.
- ديناميكيّ — يُعرَض في كل طلب، على الخادم، لأن الخرج يعتمد على شيءٍ لا يُعرَف إلا وقت الطلب.
عادةً لا تقلب مفتاحًا؛ بل يصير المسار ديناميكيًّا لحظة استخدامه واجهةً ديناميكية أو طلب بياناتٍ غير مخزّن:
import { cookies, headers } from "next/headers";
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const cookieStore = await cookies(); // واجهة ديناميكية ← عرض ديناميكيّ
const { q } = await searchParams; // مُدخَل خاصّ بالطلب ← ديناميكيّ
// ...
}مُطلِقات العرض الديناميكيّ: قراءة cookies() أو headers() أو draftMode() أو searchParams (كلها غير متزامنة في Next 15+)، أو إجراء fetch غير مخزّن. وبغيابها يبقى المسار ساكنًا. لهذا لا يعني "كل شيءٍ مكوّن خادم" أن "كل شيءٍ يعمل في كل طلب" — فمعظمه معروضٌ مسبقًا مرّةً واحدة.
الذواكر الأربع
يطبّق Next.js أربع ذواكر. وهي مستقلّة، وينبع الالتباس دائمًا تقريبًا من خلطها. من الأقرب إلى كودك خارجًا:
| الذاكرة | ما تخزّنه | أين | العمر |
|---|---|---|---|
| تحفيظ الطلب | قيم fetch في عرضٍ واحد | الخادم | طلبٌ واحد |
| ذاكرة البيانات | نتائج جلب البيانات | الخادم | دائمة (حتى إعادة التحقّق) |
| ذاكرة المسار الكاملة | HTML + حمولة RSC للمسارات الساكنة | الخادم | دائمة (حتى إعادة التحقّق/النشر) |
| ذاكرة الموجّه | حمولة RSC للمسارات المزارة | العميل | الجلسة / مؤقّتة |
تحفيظ الطلب
داخل مرور عرضٍ واحد، إن استدعيت fetch بنفس العنوان والخيارات مرّتين، يشغّله Next.js مرّةً ويعيد استخدام النتيجة. يتيح هذا جلب البيانات نفسها في تخطيطٍ وصفحةٍ بلا طلبٍ مزدوج — بلا حاجة للرفع وتنقيط الخصائص. تلقائيّ، على مستوى React، ويدوم لذلك الطلب فقط.
ذاكرة البيانات (وافتراضيّ Next 15)
تُبقي ذاكرة البيانات نتائج الجلب عبر الطلبات والنشرات، على الخادم. التفصيل الإصداريّ الحرج: في Next.js 15+، fetch غير مخزّنٍ افتراضيًّا (كان مخزّنًا في 14). تختار الدخول صراحةً:
// غير مخزّن — يُجلَب طازجًا كل طلب (افتراضيّ Next 15)
await fetch("https://api.example.com/prices");
// مخزّن إلى أجلٍ غير مسمّى في ذاكرة البيانات
await fetch("https://api.example.com/config", { cache: "force-cache" });
// مخزّن، لكن يُعاد التحقّق كل 60 ثانيةً على الأكثر (بأسلوب ISR)
await fetch("https://api.example.com/posts", { next: { revalidate: 60 } });
// مخزّن ومُوسَّم، كي تُبطِله عند الطلب
await fetch("https://api.example.com/posts", { next: { tags: ["posts"] } });فالتخزين الآن خيارٌ متعمَّد لكل طلب. الجأ إلى force-cache أو revalidate للبيانات المشتركة بطيئة التغيّر؛ واترك غير المخزّن للبيانات لكل مستخدمٍ أو الفورية.
ذاكرة المسار الكاملة
حين يكون المسار ساكنًا، يخزّن Next.js خرجه المعروض (HTML + حمولة مكوّن خادم React) وقت البناء — ذاكرة المسار الكاملة. تُخدَم الطلبات هذا الخرج المخزّن بلا أيّ عرض. ويسقط المسار من هذه الذاكرة تلقائيًّا حين يصير ديناميكيًّا (واجهة ديناميكية أو fetch غير مخزّن)، أو يُبطَل حين تعيد التحقّق من بياناته.
ذاكرة الموجّه (على العميل)
على العميل، يحتفظ Next.js بحمولة RSC للمسارات التي زرتها في الذاكرة، فيصير الرجوع فوريًّا ولا يمسّ الخادم. لهذا يبدو التنقّل على العميل فوريًّا. وهي محدودة زمنيًّا وتُمسَح عند إعادة تحميلٍ كاملة؛ وتستطيع فرض بياناتٍ طازجة بعد تعديلٍ بـ router.refresh().
إعادة التحقّق: إبقاء البيانات المخزّنة طازجة
التخزين بلا وسيلةٍ للتحديث علّة. استراتيجيتان:
زمنيّة — تتحدّث البيانات على فترة. اضبطها لكل جلب (next: { revalidate: 60 }) أو لمقطع مسارٍ كامل:
// app/blog/page.tsx — أعِد توليد هذا المسار مرّةً كل ساعةٍ على الأكثر
export const revalidate = 3600;عند الطلب — تُبطِل بالضبط حين تتغيّر البيانات الأساسية (مثلًا بعد نشر)، وهو أدقّ من تخمين فترة. استدعِ هذه من فعل خادمٍ أو معالِج مسار:
import { revalidateTag, revalidatePath } from "next/cache";
// أبطِل كل fetch مُوسَّم بـ "posts"
revalidateTag("posts");
// أبطِل ذاكرة مسارٍ محدّد
revalidatePath("/blog");إعادة التحقّق بالوسم هي القويّة: وسِّم عمليات الجلب المتعلّقة (next: { tags: ["posts"] })، فيحدّث revalidateTag("posts") واحدٌ بعد تعديلٍ كلَّها، أينما كانت في التطبيق. هذا عمود موقعٍ سريعٍ يبقى مع ذلك محدّثًا دائمًا.
تخزين بياناتٍ غير fetch
تكاملات fetch تلقائية، لكن استعلامات قاعدة البيانات والعمل غير المتزامن الآخر ليست fetch. غلّفها بـ unstable_cache لوضع نتائجها في ذاكرة البيانات، بالوسوم وإعادة التحقّق نفسها:
import { unstable_cache } from "next/cache";
const getPosts = unstable_cache(
async () => db.post.findMany(),
["all-posts"], // أجزاء مفتاح التخزين
{ tags: ["posts"], revalidate: 3600 },
);الآن getPosts() مخزّنٌ ويُعاد التحقّق منه كـ fetch مُوسَّم — وrevalidateTag("posts") يمسحه أيضًا. (في Next.js 16 البديل الحديث هو توجيه 'use cache' المستقرّ — أضِف 'use cache' أعلى الدالّة مع cacheLife وcacheTag من next/cache؛ والمفهوم متطابق — علّم نتيجة دالّةٍ قابلةً للتخزين ووسِّمها. ويبقى unstable_cache متاحًا للنموذج السابق.)
تهيئة مقطع المسار
كل layout.tsx/page.tsx يستطيع تصدير تهيئةٍ تتحكّم في العرض والتخزين لمقطعه كله:
export const dynamic = "force-dynamic"; // أدخِل المسار كله إلى العرض الديناميكيّ
// export const dynamic = "force-static"; // افرِض ساكنًا، وأخطئ عند الواجهات الديناميكية
export const revalidate = 60; // إعادة تحقّقٍ زمنية على مستوى المقطع
export const fetchCache = "default-cache"; // تجاوَز افتراضيّ كل fetch
export const runtime = "edge"; // شغّل على بيئة Edge بدل Nodedynamic = "force-dynamic" منفذ الهروب حين تريد مسارًا معروضًا لكل طلبٍ مهما فعل؛ وforce-static الضمان المعاكس. فضّل ترك Next.js يستنتج من خيارات بياناتك، والجأ لهذه فقط للتجاوز.
التدفّق بـ Suspense وloading.tsx
لا يجب أن يحجب العرض الديناميكيّ الصفحة كلها. بـالتدفّق، يُرسِل Next.js القشرة الساكنة فورًا ويدفق الأجزاء البطيئة حين تُحلّ بياناتها. يغلّف loading.tsx المسار بحدّ Suspense تلقائيًّا؛ ولتحكّمٍ أدقّ، غلّف المكوّنات البطيئة الفردية بـ <Suspense>:
import { Suspense } from "react";
export default function Page() {
return (
<section>
<Header /> {/* يُعرَض فورًا */}
<Suspense fallback={<FeedSkeleton />}>
<Feed /> {/* يتدفّق حين تجهز بياناته */}
</Suspense>
</section>
);
}يرى المستخدم القشرة والترويسة معًا، مع هيكلٍ عظميّ حيث ستهبط الخلاصة — أداءٌ محسوسٌ أفضل بكثير من انتظار أبطأ استعلامٍ قبل إظهار أيّ شيء.
العرض المسبق الجزئيّ (PPR)
القطعة التي تربط كل شيء. يتيح العرض المسبق الجزئيّ لمسارٍ واحدٍ أن يكون ساكنًا في معظمه بثقوبٍ ديناميكية: يعرض Next.js مسبقًا قشرةً ساكنة ويدفق الأجزاء الديناميكية (المغلّفة بـ <Suspense>) إليها وقت الطلب. تحصل على الرسم الأول الفوريّ لصفحةٍ ساكنة و بياناتٍ لكل طلبٍ على المسار نفسه، بلا اختيار أحدهما للصفحة كلها.
اعتبارًا من Next.js 16، صار العرض المسبق الجزئيّ مستقرًّا وهو السلوك الافتراضيّ لـمكوّنات التخزين (Cache Components)، التي تفعّلها بـ cacheComponents: true في next.config.ts. في ذلك النموذج تنتقل قصة التخزين من خيارات fetch إلى توجيه 'use cache': تعلّم دوال البيانات أو مكوّناتٍ كاملة قابلةً للتخزين بـ 'use cache' (مع cacheLife/cacheTag)، وتغلّف أيّ شيءٍ يقرأ واجهةً وقت التشغيل (cookies/headers/searchParams) أو يحتاج بياناتٍ طازجة بـ <Suspense>، فيعرض Next الباقي مسبقًا في القشرة الساكنة. النموذج رباعيّ الذواكر أعلاه لا يزال يصف الإعداد الافتراضيّ (بلا مكوّنات التخزين)؛ ومكوّنات التخزين هي التطوّر الأحدث المدفوع بالتوجيهات — نفس المبدأ، وواجهةٌ أنظف: ساكنٌ افتراضيًّا، ديناميكيّ بالضبط حيث تغلّف.
الأخطاء الشائعة
- افتراض أن
fetchمخزّن (عادة Next 14) — في Next 15+ ليس كذلك؛ ادخل بـcache: "force-cache"أوnext: { revalidate }. - توقّع بياناتٍ طازجة من صفحةٍ ساكنة — المسار الساكن كليًّا يخدم HTML مخزّنًا؛ استخدم
revalidateأو وسمًا أو واجهةً ديناميكية إن وجب أن يعكس التغييرات. - جعل مسارٍ كامل ديناميكيًّا بالخطأ — استدعاء
cookies()/headers()واحد أو fetch غير مخزّنٍ عاليًا في الشجرة يُخرِج المقطع كله من العرض الساكن. ادفع القراءات الخاصّة بالطلب لأسفل وغلّفها بـ<Suspense>. - تخمين فترةٍ حين يكون الطلب هو الصواب — إن عرفت متى تتغيّر البيانات، فـ
revalidateTagبعد التعديل يتفوّق على نافذةrevalidateقصيرة. - تخزين بياناتٍ لكل مستخدم — لا تخزّن أبدًا بـ
force-cacheطلبًا يتضمّن رمز/كوكي المستخدم الحاليّ؛ ستخدم بيانات مستخدمٍ لآخر. - نسيان ذاكرة موجّه العميل — بعد تعديل، قد يُظهِر العميل مسارًا مزارًا قديمًا؛
router.refresh()(أو إعادة التحقّق من فعل خادم) يزامنه. - عدم وسم عمليات الجلب — البيانات المخزّنة غير المُوسَّمة لا يمكن إلا إعادة التحقّق منها زمنيًّا؛ وسِّمها كي يُبطِل تعديلٌ بالضبط ما تغيّر.
تمارين
جرّب كلًّا قبل فتح الحلّ.
تمرين 1 — ساكن أم ديناميكيّ؟
هل تُعرَض هذه الصفحة ساكنةً أم ديناميكيةً، ولماذا؟
export default async function Page() {
const res = await fetch("https://api.example.com/data", { next: { revalidate: 300 } });
return <List items={await res.json()} />;
}اعرض الحل
ساكنة (مع ISR). لا تستخدم واجهةً ديناميكية، والجلب مخزّنٌ بإعادة تحقّقٍ 300 ثانية — فيعرض Next المسار مسبقًا ويعيد توليده في الخلفية كل 5 دقائق على الأكثر. لا cookies()/headers()/searchParams، ولا fetch غير مخزّن، فلا شيء يفرض العرض الديناميكيّ.
تمرين 2 — خزّن ثم أبطِل
خزّن fetch لقائمة التدوينات كي يُبطَل عند الطلب، وأظهِر الاستدعاء الذي يُبطِله بعد النشر.
اعرض الحل
await fetch("https://api.example.com/posts", { next: { tags: ["posts"] } });
// لاحقًا، في فعل خادم النشر:
import { revalidateTag } from "next/cache";
revalidateTag("posts");وسم الجلب يتيح لـ revalidateTag("posts") واحدٍ تحديث كل موضعٍ تُستخدَم فيه تلك البيانات، بالضبط حين تتغيّر.
تمرين 3 — دفِّق قسمًا بطيئًا
ترويسة الصفحة فورية لكن أداة التحليلات بطيئة. هيكِلها كي تظهر الترويسة فورًا.
اعرض الحل
<>
<Header />
<Suspense fallback={<WidgetSkeleton />}>
<Analytics /> {/* ينتظر بياناته البطيئة الخاصّة */}
</Suspense>
</>حدّ <Suspense> يتيح لـ Next تدفيق القشرة الساكنة (بالترويسة) أولًا وإرسال أداة التحليلات حين تُحلّ بياناتها.
النموذج الذهني الذي تحتفظ به
يعرض Next.js كل مسارٍ إمّا ساكنًا (مسبقًا، HTML مخزّن — الافتراضيّ) أو ديناميكيًّا (لكل طلب)، ويتحوّل المسار إلى ديناميكيّ لحظة قراءته واجهةً ديناميكية (cookies/headers/searchParams) أو إجرائه fetch غير مخزّن. وبين كودك والمستخدم تجلس أربع ذواكر: تحفيظ الطلب (يزيل تكرار الجلب في عرضٍ واحد)، وذاكرة البيانات (نتائج جلبٍ دائمة — اختياريّ الدخول في Next 15+ عبر force-cache/revalidate/tags)، وذاكرة المسار الكاملة (المسارات الساكنة المعروضة)، وذاكرة الموجّه على العميل (المسارات المزارة). أبقِ البيانات طازجةً بـإعادة تحقّقٍ زمنية revalidate أو، أفضل، عند الطلب بـ revalidateTag/revalidatePath بعد تعديل؛ وخزّن العمل غير fetch بـ**unstable_cache؛ واستخدم التدفّق (loading.tsx / <Suspense>) والعرض المسبق الجزئيّ** لخدمة قشرةٍ ساكنة فورًا بينما تتدفّق الأجزاء الديناميكية. أجِب عن "أيّ نمط عرض، أيّ ذاكرة؟" لأيّ مسارٍ وبياناته، فيصير نموذج أداء موجّه التطبيق كلّه بين يديك.