نموذج العرض في App Router من Next.js، مشروحًا كما يجب
الثابت والديناميكي والمتدفّق والمُعاد التحقّق ليست أربعة إعدادات — بل نموذج واحد بمفاتيح قليلة. معرفة أيّ مفتاح قلبته هي الجزء الأكبر من عمل الأداء في Next.js.
تكاد كل مشكلة أداء في Next.js نُستدعى لتشخيصها تختصر في الجملة نفسها: لم يكن أحد يعلم أن هذا المسار صار ديناميكيًا.
المشكلة ليست في العرض الديناميكي بحد ذاته. المشكلة أن الانتقال من الثابت إلى الديناميكي يحدث ضمنيًا. يحدث لأن أحدهم قرأ ملف تعريف ارتباط على عمق أربعة مكوّنات، ولم يذكر ذلك شيء في طلب الدمج.
هذه المقالة هي النموذج الذهني الذي نعلّمه لكل فريق نعمل معه.
لا توجد أربعة أنماط للعرض
يوجد نموذج واحد — عرض شجرة المكوّنات على الخادم — والسؤال الحقيقي الوحيد هو متى يحدث هذا العرض وكم منه يُعاد استخدامه.
| متى يحدث العرض | ما يُسمّى به عادةً |
|---|---|
| وقت البناء، يُعاد استخدامه دائمًا | ثابت |
| وقت البناء، يُستبدل عند إشارة | Incremental Static Regeneration |
| عند كل طلب | ديناميكي |
| البناء للقشرة، والطلب للثغرات | Partial Prerendering |
التدفّق (Streaming) متعامد على كل ذلك. التدفّق يتعلّق بـترتيب التسليم، لا بوقت تنفيذ العمل.
ما الذي يجعل المسار ديناميكيًا فعلًا
المسار ثابت إلى أن يحتاج شيء بداخله إلى الطلب. قراءة أيٍّ من هذه تُخرج المسار من العرض الثابت:
import { cookies, headers } from 'next/headers';
import { connection } from 'next/server';
await cookies(); // needs the request
await headers(); // needs the request
await connection(); // explicitly asks for the requestوsearchParams في خصائص الصفحة تفعل الشيء نفسه. وكذلك fetch مع
cache: 'no-store'، وكذلك أي استدعاء لـunstable_noStore بقي من إعادة هيكلة
سابقة.
والمهم أن هذا يُعدي إلى الأعلى لا إلى الأسفل. مكوّن خادم عميق في الشجرة
يقرأ cookies() يجعل المسار كله ديناميكيًا، لأن المسار لا يمكن عرضه مسبقًا
إذا كان أي جزء منه يحتاج طلبًا لم يوجد بعد.
لهذا لا يكون سؤال التدقيق أبدًا «هل هذه الصفحة ثابتة؟». السؤال هو: «ما أعمق شيء في هذه الشجرة يلمس الطلب، وهل يحتاج إلى ذلك؟»
الحالة العرضية الشائعة
// app/products/[slug]/page.tsx
export default async function Page({ params }) {
const { slug } = await params;
const product = await getProduct(slug);
return (
<>
<ProductDetail product={product} />
<RecentlyViewed /> {/* reads cookies() — the whole page is now dynamic */}
</>
);
}كان يمكن لتفاصيل المنتج أن تبقى ثابتة لعام كامل. شريط واحد مخصّص في الزاوية جعل كل طلب يُعيد عرض كل شيء.
الحل ليس حذف الشريط، بل دفعه خلف حدود Suspense كي تتمكّن بقية الصفحة من العرض المسبق:
<Suspense fallback={<RecentlyViewedSkeleton />}>
<RecentlyViewed />
</Suspense>مع تفعيل Partial Prerendering، تُقدَّم القشرة الثابتة من الحافة فورًا ولا تُحسب إلا الثغرة مع كل طلب. وبدونه، تحصل على الأقل على القشرة متدفّقة أولًا بدل أن تنتظر الصفحة كلها.
إعادة التحقّق إشارة، لا مؤقّت
تلجأ معظم الفرق إلى revalidate زمني لأنه أول خيار في التوثيق:
export const revalidate = 3600;هذا تخمين لمعدّل تغيّر محتواك. أما إعادة التحقّق بالوسوم فهي حقيقة عن وقت تغيّره:
// Reading side
const posts = await fetch(`${API}/posts`, {
next: { tags: ['posts'] },
});
// Writing side — your CMS webhook route
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
await verifyWebhookSignature(request);
revalidateTag('posts');
return Response.json({ revalidated: true });
}الفرق عمليًا: مع مؤقّت مدته ساعة، ينشر محرّرك تصحيحًا ثم يظل يُحدّث الصفحة خمسين دقيقة. ومع وسم، يظهر التصحيح خلال ثوانٍ، وتُقدَّم الصفحة من ذاكرة التخزين المؤقت إلى ما لا نهاية فيما عدا ذلك. أطزج وأرخص في آنٍ واحد.
حفظ الطلب ليس ذاكرة البيانات
يُخلط بينهما باستمرار، وهما يحلّان مشكلتين مختلفتين.
حفظ الطلب يزيل تكرار استدعاءات fetch المتطابقة داخل مسار عرض واحد.
وُجد كي تستطيع استدعاء getUser() في التخطيط ومرة أخرى في الصفحة دون دفع
رحلتين إلى الخادم. إنه لكل عملية عرض ويتبخّر بانتهائها.
ذاكرة البيانات تبقى عبر الطلبات وعمليات النشر. وعليها يعمل revalidate
وrevalidateTag.
إذا غلّفت مصدر بيانات لا يستخدم fetch — عميل قاعدة بيانات مثلًا — فلن تحصل
على الحفظ مجانًا. استخدم cache من React:
import { cache } from 'react';
import 'server-only';
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});بدون هذا الغلاف، يُصدر تخطيط وثلاثة مكوّنات يحتاج كلٌّ منها المستخدم الحالي أربعة استعلامات متطابقة لكل عملية عرض. وجدنا هذا بالضبط في الإنتاج مرات أكثر مما نودّ الاعتراف به.
كيف تدقّق مسارًا في خمس دقائق
- شغّل
next buildواقرأ جدول المسارات. الرمز○يعني ثابتًا، و●يعني SSG بمعاملات، وƒيعني ديناميكيًا. كل ما جاءƒعلى غير المتوقّع هو قائمتك. - لكل مفاجأة، فتّش الشجرة عن
cookiesوheadersوsearchParamsوno-storeوconnection. - اسأل: هل هذه البيانات لازمة لـأول رسم للشاشة أم لجزء مخصّص فقط؟ الأجزاء المخصّصة تذهب خلف Suspense.
- تحقّق أن كل دالة بيانات لا تستخدم
fetchمغلّفة بـcache. - استبدل
revalidateالزمني بالوسوم حيثما وُجد مسار كتابة.
هذه القائمة هي الساعة الأولى من معظم مشاريع الأداء التي نُنفّذها، وفيها عادةً يختبئ أكبر مكسب منفرد.
ماذا يمنحك هذا
المسار الذي يُعرض ثابتًا يقدّم HTML الخاص به من عقدة حافة في شبكة توصيل المحتوى خلال أجزاء من الألف من الثانية برقم واحد. والمسار نفسه معروضًا ديناميكيًا يُشغّل شجرة مكوّناتك وطبقة بياناتك وقاعدة بياناتك عند كل طلب. الفرق ليس نسبة مئوية، بل رتبة مقدار كاملة، ويظهر في مؤشر LCP، وفي تكلفة البنية التحتية، وفيما يحدث لموقعك حين ينتشر رابط انتشارًا واسعًا بعد ظهر يوم ثلاثاء.
