Next.js: استراتيجيات التصيير: SSR و SSG و ISR
آخر تحديث: 2026-08-26
تمنحك استراتيجيات التصيير في Next.js خيارًا بين "جاهز وقت البناء" و"حسب الطلب" — المفتاح هو اختيار الوضع المناسب.
1. ما ستتعلمه
- أول بايت في الوقت الفعلي عند الحدود بين تصيير SSR المتدفق و Suspense
- توليد المواقع الثابتة SSG و
generateStaticParams()للتوليد المسبق للمسارات الديناميكية - التجديد الثابت المتزايد ISR ونافذة
revalidateالزمنية - تكوين مقطع المسار
force-dynamicمقابلforce-static - تفعيل ISR حسب الطلب باستخدام
revalidatePath()/revalidateTag()
2. قصة حقيقية لمدير تقني
(1) نقطة الألم: صفحات CMS تستغرق 6 ثوانٍ للتحميل، وفريق العمليات يحدثها 50 مرة يوميًا
تشارلي هو القائد التقني لفريق TaskFlow. يحتوي نظام CMS للتجارة الإلكترونية في الشركة على 10,000 صفحة منتج، كل منها تحتوي على وصف وسعر ومخزون وصور. الأسوأ هو:
| السؤال | البيانات |
|---|---|
| زمن استجابة SSR للصفحة الواحدة | 6 ثوانٍ (كل طلب يستعلم قاعدة البيانات) |
| تحديثات العمليات اليومية | 50+ تحديثًا (الأسعار/المخزون/العروض الترويجية) |
| وحدة المعالجة المركزية للخادم | 85%+ باستمرار |
| سياسة التخزين المؤقت | ❌ لا توجد — يتم التصيير في الوقت الفعلي كل مرة |
يقوم فريق العمليات بتحديث الأسعار كل 15 دقيقة، لكن SSR يضطر إلى استعلام قاعدة البيانات من جديد كل مرة — 10,000 صفحة × 6 ثوانٍ = 60,000 ثانية من عبء وحدة المعالجة المركزية يوميًا.
(2) حلول مشاكل ISR
عند البناء باستخدام ISR، قم بتوليد صفحات ثابتة، وأعد التحقق كل N ثانية، وقم بالتحديث فورًا حسب الطلب.
// app/products/[id]/page.tsx — التجديد الثابت المتزايد ISR
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } })
return products.map(p => ({ id: String(p.id) }))
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: Number(params.id) } })
return <ProductView product={product} />
}
// تكوين مقطع المسار
export const revalidate = 300 // إعادة التحقق كل 5 دقائق
(3) النتائج
| البُعد | SSR النقي | ISR |
|---|---|---|
| زمن الاستجابة | 6 ثوانٍ | < 50 مللي ثانية (HTML ثابت) |
| وحدة المعالجة المركزية للخادم | 85% | < 10% |
| تحديث العمليات | ❌ يجب الانتظار 6 ثوانٍ | ✅ فوري (إعادة التحقق حسب الطلب) |
| التخزين المؤقت CDN | ❌ غير مدعوم | ✅ تخزين كامل للصفحة مدعوم |
| استعلامات قاعدة البيانات في الثانية | 10,000/ساعة | ~50/ساعة |
3. SSR (التصيير من جانب الخادم)
يقوم SSR (التصيير من جانب الخادم) بتصيير HTML على الخادم لكل طلب. يستخدم Next.js 16 تصيير SSR المتدفق من React 18 — بدلاً من انتظار اكتمال تصيير الصفحة بأكملها، يقوم بتقسيم الصفحة إلى حدود Suspense متعددة ويرسلها إلى العميل واحدة تلو الأخرى.
sequenceDiagram
participant Client as المتصفح
participant Server as خادم Next.js
participant DB as قاعدة البيانات
Client->>Server: GET /dashboard
Server->>Client: إرسال هيكل HTML الثابت (فوري)
Server->>DB: استعلام البيانات (متوازي)
DB-->>Server: إرجاع مجموعة فرعية من البيانات
Server->>Client: إرسال متدفق لحد Suspense 1
DB-->>Server: تحميل المزيد من البيانات
Server->>Client: إرسال متدفق لحد Suspense 2
Client->>Client: عرض المحتوى تدريجيًا
| التكوين | الصيغة | السلوك |
|---|---|---|
| الافتراضي (SSR المتدفق) | بدون تكوين | تصيير صفحة ديناميكي، يدعم التدفق عبر Suspense |
force-dynamic |
export const dynamic = 'force-dynamic' |
فرض إعادة التصيير مع كل طلب؛ تعطيل التخزين المؤقت |
force-static |
export const dynamic = 'force-static' |
فرض التجميع الثابت أثناء البناء؛ تعطيل السلوك الديناميكي |
(1) dynamic = 'force-dynamic'
يضمن إعادة توليد الصفحة مع كل طلب، مما يجعله مناسبًا للبيانات شديدة التخصيص (لوحات تحكم المستخدم، عربات التسوق).
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
const user = await getCurrentUser() // الحصول على أحدث البيانات مع كل طلب
return <DashboardView user={user} />
}
(2) dynamic = 'force-static'
يتم فرض تصيير الصفحات بشكل ثابت أثناء وقت البناء؛ حتى إذا كانت تحتوي على دوال ديناميكية، فسيتم حساب هذه الدوال مسبقًا.
// app/about/page.tsx
export const dynamic = 'force-static'
export default async function AboutPage() {
const version = await getVersion() // الحساب مرة واحدة أثناء وقت البناء
return <div>Version: {version}</div>
}
▶ مثال: تجربة تحميل SSR المتدفق (المستوى: ⭐)
الناتج:
يعرض واجهة مستخدم مكون AboutPage.
// app/streaming-demo/page.tsx
import { Suspense } from 'react'
export default function StreamingDemoPage() {
return (
<div>
<h1>Streaming SSR Demo</h1>
<p>يظهر هذا النص فورًا (هيكل ثابت).</p>
<Suspense fallback={<div>جارٍ تحميل البيانات البطيئة...</div>}>
<SlowComponent delay={3000} />
</Suspense>
<Suspense fallback={<div>جارٍ تحميل البيانات السريعة...</div>}>
<SlowComponent delay={1000} />
</Suspense>
</div>
)
}
async function SlowComponent({ delay }: { delay: number }) {
await new Promise(resolve => setTimeout(resolve, delay))
return <div>تم التحميل بعد {delay} مللي ثانية</div>
}
الناتج:
يعرض هيكلًا ثابتًا فورًا، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
الاحتياطي: جارٍ تحميل البيانات البطيئة...
النص المرئي: Streaming SSR Demo | يظهر هذا النص فورًا (هيكل ثابت). | جارٍ تحميل البيانات البطيئة... | }>
4. SSG توليد المواقع الثابتة و generateStaticParams
يقوم SSG (توليد المواقع الثابتة) بتوليد جميع صفحات HTML دفعة واحدة أثناء عملية البناء، مما يجعله مناسبًا للسيناريوهات التي يتغير فيها المحتوى بشكل غير متكرر (المدونات، التوثيق، صفحات التسويق).
(1) الصفحة الثابتة
// app/docs/page.tsx — يتم التوليد أثناء البناء
export default async function DocsPage() {
const docs = await fetch('https://cms.example.com/docs', {
cache: 'force-cache' // الاسترجاع أثناء البناء، تخزين مؤقت دائم
}).then(r => r.json())
return <ArticleView content={docs} />
}
(2) generateStaticParams() التوليد المسبق للمسارات الديناميكية
يُستخدم للصفحات مثل [id] و [slug] التي تستخدم التوجيه الديناميكي؛ يعيد جميع معاملات المسار الممكنة، ويتم التوليد مرة واحدة أثناء وقت البناء.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts').then(r => r.json())
return posts.map((post: any) => ({ slug: post.slug })) // → /blog/hello-world, /blog/nextjs-guide ...
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.slug}`).then(r => r.json())
return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}
▶ مثال: التوليد المسبق للتوجيه الديناميكي SSG (المستوى: ⭐⭐)
الناتج:
يجلب البيانات ويعرض قائمة بالعناصر.
// app/products/[id]/page.tsx
type Product = { id: number; title: string; price: number }
export async function generateStaticParams() {
const products: Product[] = await fetch('https://fakestoreapi.com/products').then(r => r.json())
return products.slice(0, 5).map(p => ({ id: String(p.id) }))
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product: Product = await fetch(`https://fakestoreapi.com/products/${params.id}`).then(r => r.json())
return (
<div>
<h1>{product.title}</h1>
<p>السعر: ${product.price}</p>
</div>
)
}
الناتج:
سجل البناء:
✓ Generating static pages (5/5) /products/1 /products/2 /products/3 /products/4 /products/5
الناتج:
يعرض المتصفح كل صفحة منتج:
/products/1 → عنوان المنتج + "السعر: $109.95"
/products/2 → عنوان المنتج + "السعر: $22.30"
/products/3 → عنوان المنتج + "السعر: $55.99"
/products/4 → عنوان المنتج + "السعر: $15.99"
/products/5 → عنوان المنتج + "السعر: $695.00"
5. ISR التجديد الثابت المتزايد
يقع ISR (التجديد الثابت المتزايد) بين SSG و SSR: يتم توليد الصفحات الثابتة أثناء وقت البناء، ويتم تشغيل إعادة التوليد في الخلفية بعد انتهاء نافذة revalidate، لذلك يرى المستخدمون دائمًا الصفحات المخزنة مؤقتًا.
graph LR
A[البناء] --> B[توليد HTML ثابت]
B --> C[ذاكرة CDN المؤقتة]
C --> D[طلب المستخدم]
D --> E{انتهت صلاحية<br/>revalidate؟}
E -->|لا| F[إرجاع من الذاكرة المؤقتة]
E -->|نعم| G[إرجاع من الذاكرة المؤقتة + إعادة التوليد في الخلفية]
G --> B
| الوضع | revalidate | السلوك | الزيارة الأولى | تأخير التحديث |
|---|---|---|---|---|
| SSG | لا يوجد | التوليد فقط أثناء البناء | فوري | يتطلب إعادة بناء |
| ISR | revalidate: 60 |
التحديث في الخلفية بعد 60 ثانية | فوري | ≤ 60 ثانية |
| SSR | dynamic: 'force-dynamic' |
التحديث مع كل طلب | توليد ديناميكي | فوري |
(1) تكوين revalidate لمقطع المسار
// app/posts/[id]/page.tsx
export const revalidate = 3600 // إعادة التحقق مرة كل ساعة
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.id}`, {
next: { revalidate: 3600 } // متوافق أيضًا على مستوى fetch
}).then(r => r.json())
return <PostView post={post} />
}
(2) ISR حسب الطلب (إعادة التحقق حسب الطلب)
استخدم revalidatePath() أو revalidateTag() لتشغيل إعادة التوليد الفوري عند تغير البيانات.
// app/admin/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updatePost(formData: FormData) {
const id = formData.get('id') as string
await db.post.update({ where: { id: Number(id) }, data: { title: formData.get('title') as string } })
revalidatePath(`/posts/${id}`) // تحديث مقال واحد
revalidatePath('/posts') // تحديث صفحة القائمة
revalidateTag('posts') // تحديث كل ما يحمل وسم posts في الذاكرة المؤقتة
}
▶ مثال: نافذة ISR الزمنية (المستوى: ⭐⭐)
الناتج:
يعرض واجهة مستخدم مكون updatePost.
// app/isr-demo/page.tsx
export const revalidate = 30 // إعادة التحقق الفوري كل 30 ثانية
export default async function IsrDemoPage() {
const time = await fetch('http://worldtimeapi.org/api/timezone/Asia/Shanghai', {
next: { tags: ['time'] }
}).then(r => r.json())
return (
<div>
<h1>ISR Demo — إعادة التحقق كل 30 ثانية</h1>
<p>الوقت الحالي: {time.datetime}</p>
<p>تم التوليد في: {new Date().toISOString()}</p>
</div>
)
}
الناتج:
يجلب البيانات ويعرض النتيجة.
النص المرئي: ISR Demo — إعادة التحقق كل 30 ثانية | الوقت الحالي: {time.datetime}
▶ مثال: إعادة التحقق من مسار API حسب الطلب (المستوى: ⭐⭐⭐)
الناتج:
يتم تصيير الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
const body = await request.json()
if (body.type === 'path') {
revalidatePath(body.path)
} else if (body.type === 'tag') {
revalidateTag(body.tag)
}
return NextResponse.json({ revalidated: true })
}
# من استدعاء Webhook لنظام CMS
curl -X POST https://example.com/api/revalidate \
-H "x-revalidate-secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"type": "tag", "tag": "posts"}'
الناتج:
{"status":"ok","data":{}}
▶ مثال: dynamic = 'force-static' لإعداد صفحة ثابتة (المستوى: ⭐)
الناتج:
يتم تصيير الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
// app/static-page/page.tsx — فرض الثبات
export const dynamic = 'force-static'
export default async function StaticPage() {
const time = new Date().toISOString()
return (
<div>
<h1>Static Page (تم بناؤها وقت البناء)</h1>
<p>هذا الطابع الزمني ثابت: {time}</p>
<p>قم بتحديث الصفحة — الوقت لا يتغير أبدًا.</p>
</div>
)
}
الناتج:
يعرض: Static Page (تم بناؤها وقت البناء)
النص المرئي: Static Page (تم بناؤها وقت البناء) | هذا الطابع الزمني ثابت: {time} | قم بتحديث الصفحة — الوقت لا يتغير أبدًا.
// app/dynamic-page/page.tsx — تحديثات إلزامية
export const dynamic = 'force-dynamic'
export default async function DynamicPage() {
const time = new Date().toISOString()
return (
<div>
<h1>Dynamic Page (يتم تصييرها مع كل طلب)</h1>
<p>هذا الطابع الزمني يتغير مع كل تحديث: {time}</p>
</div>
)
}
مقارنة الناتج:
الصفحة الثابتة: الطابع الزمني دائمًا "2026-07-06T10:00:00.000Z" ← لا يتغير
الصفحة الديناميكية: الطابع الزمني يتغير مع كل تحديث ← يختلف في كل مرة
6. مثال كامل: نظام مدونة بثلاثة أوضاع
// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>{children}</div>
}
// app/blog/page.tsx — قائمة المدونة (SSG + ISR)
export const revalidate = 300 // ISR كل 5 دقائق
export default async function BlogListPage() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['blog-posts'] }
}).then(r => r.json())
return (
<div>
<h1>المدونة</h1>
<ul>{posts.slice(0, 10).map((p: any) => (
<li key={p.id}><a href={`/blog/${p.id}`}>{p.title}</a></li>
))}</ul>
<p>آخر توليد: {new Date().toISOString()}</p>
</div>
)
}
// app/blog/[id]/page.tsx — تفاصيل المقال (SSG + ISR)
export async function generateStaticParams() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json())
return posts.slice(0, 10).map((p: any) => ({ id: String(p.id) }))
}
export default async function BlogPostPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, {
next: { tags: [`post-${params.id}`] }
}).then(r => r.json())
const comments = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}/comments`, {
cache: 'no-store'
}).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<h2>التعليقات ({comments.length})</h2>
<ul>{comments.map((c: any) => (
<li key={c.id}><strong>{c.name}:</strong> {c.body}</li>
))}</ul>
</article>
)
}
// app/blog/actions.ts — التحديث حسب الطلب
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshBlog() {
revalidateTag('blog-posts')
}
// app/blog/[id]/admin/page.tsx — لوحة الإدارة (SSR)
export const dynamic = 'force-dynamic'
export default async function AdminPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`).then(r => r.json())
return (
<form action={async (fd) => {
'use server'
await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, { method: 'PATCH', body: JSON.stringify({ title: fd.get('title') }) })
revalidateTag(`post-${params.id}`)
}}>
<input name="title" defaultValue={post.title} />
<button type="submit">تحديث</button>
</form>
)
}
❓ أسئلة شائعة
generateStaticParams إلى استغراق البناء وقتًا طويلاً إذا قام بتوليد عدد كبير من الصفحات؟export const dynamicParams = true (افتراضي) للسماح بالتصيير الديناميكي للمسارات غير المولدة مسبقًا.<Suspense> هو وحدة إخراج متدفقة مستقلة. يرسل الخادم أولاً هيكل HTML ثابت، ثم يرسل تدريجيًا محتوى كل حد Suspense. يرى المستخدمون تأثير "تحميل تدريجي" — لا يحتاجون إلى انتظار تجهز جميع البيانات.force-dynamic و cache: 'no-store'؟force-dynamic هو تكوين مقطع مسار (الصفحة بأكملها ديناميكية)، بينما cache: 'no-store' هو تكوين جلب بيانات (عمليات الجلب الفردية لا يتم تخزينها مؤقتًا). force-dynamic يعطل جميع عمليات التخزين المؤقت للبيانات لتلك الصفحة. يُنصح بإعطاء الأولوية لضبط التخزين المؤقت الدقيق (مثل الجمع بين no-store و force-cache) بدلاً من استخدام force-dynamic كحل واحد يناسب الجميع.revalidatePath أم revalidateTag؟revalidateTag أفضل — يمسح فقط إدخالات الذاكرة المؤقتة المطابقة لذلك الوسم ولا يؤثر على البيانات الأخرى. revalidatePath يتطلب المرور على جميع الإدخالات المخزنة مؤقتًا على طول المسار، مما يتسبب في عبء أكبر. نوصي بإنشاء نظام وسوم ذي معنى واستخدام tags للتحكم الدقيق.📖 ملخص
- يحقق تصيير SSR المتدفق أول بايت فوري وتحميل تدريجي من خلال حدود Suspense
dynamic = 'force-static'/'force-dynamic'يتحكمان في وضع تصيير الصفحةgenerateStaticParams()يولد مسبقًا جميع مجموعات الصفحات للتوجيه الديناميكي أثناء البناء- ISR ينفذ تحديثات تزايدية تلقائية للصفحات الثابتة باستخدام
revalidate = N - ISR حسب الطلب يستخدم
revalidatePath()/revalidateTag()للتحديث الفوري عند تغير البيانات - هرم اختيار استراتيجية التصيير: SSG (غير متغير) → ISR (متغير أحيانًا) → SSR (متغير فوريًا) → PPR (متغير جزئيًا)
📝 تمارين
-
تمرين أساسي (⭐): أنشئ
app/ssg-demo/[id]/page.tsxواستخدمgenerateStaticParamsلتوليد خمس صفحات ثابتة مسبقًا —/products/1حتى/products/5(البيانات من FakeStore API). بعد البناء، تحقق من أن HTML لكل صفحة هو ملف ثابت. -
تمرين متقدم (⭐⭐): أنشئ صفحة عرض وقت ISR
app/isr-clock/page.tsx، وقم بتكوينrevalidate = 15، واستخدم World Time API لعرض الوقت الحالي. قم بتحديث الصفحة كل 15 ثانية للتحقق من تحديث الوقت. أضف Server Action باسمforceRefresh()يستخدمrevalidatePath()لتحديث الصفحة فورًا. -
تحدي (⭐⭐⭐): قم ببناء نظام مدونة كامل:
app/blog/page.tsx(قائمة ISR، إعادة التحقق كل 120 ثانية)،app/blog/[slug]/page.tsx(بيانات SEO الوصفية + بيانات JSON-LD المنظمة)،app/api/revalidate/route.ts(نقطة نهاية Webhook لاستقبال إشعارات تغيير CMS، تحديث الذاكرة المؤقتة بالوسم). قدم نموذجًا لاستدعاء Webhook.