Next.js: جلب البيانات: fetch و RSC
آخر تحديث: 2026-08-26
في RSC، لم يعد
fetchمجرد جلب من المتصفح — بل يمتد ليشمل طبقة التخزين المؤقت، مما يتيح لك التحكم في دورة حياة البيانات بطريقة تعريفية.
1. ما ستتعلمه
- سلوك التخزين المؤقت التلقائي لـ
fetch(url, options)في Server Component - ثلاثة أنماط للتخزين المؤقت:
force-cache،no-store،revalidate next: { tags }وrevalidateTag()لإعادة التحقق عند الطلب- جلب البيانات المتوازي
Promise.all()لتجنب تأثير الشلال (Waterfall) - تحديد وتحسين تدفقات الشلال التسلسلية
2. قصة حقيقية لمطور Full-Stack
(1) نقطة الألم: لوحة التحكم تستغرق 8 ثوانٍ للتحميل
Bob هو القائد التقني لفريق TaskFlow. تحتاج صفحة Dashboard إلى تحميل خمسة مصادر بيانات: إحصائيات المستخدمين، إجمالي عدد المشاريع، المهام الأخيرة، سجلات النشاط، وإشعارات النظام. استخدم الكود الأولي خمس استدعاءات await fetch(...) تسلسلية، كل منها ينتظر اكتمال السابق — مما أدى إلى وقت إجمالي قدره 2.1s + 1.8s + 1.5s + 0.9s + 1.7s = 8 ثوانٍ. اشتكى المستخدمون من أن الصفحة "تستغرق وقتًا طويلاً جدًا للتحميل". والأسوأ من ذلك، كان يتم إعادة الاستعلام عن API مع كل تحديث، مما تسبب في ارتفاع حمل قاعدة البيانات إلى 5,000 QPS.
(2) حل Next.js fetch
استخدم
Promise.all()للطلبات المتوازية +next: { revalidate: 60 }لتخزين مؤقت لمدة 60 ثانية.
// app/dashboard/page.tsx
export default async function DashboardPage() {
const [users, projects, tasks, logs, notifs] = await Promise.all([
fetch('https://api.example.com/stats/users', { next: { revalidate: 60 } }),
fetch('https://api.example.com/stats/projects', { next: { revalidate: 60 } }),
fetch('https://api.example.com/stats/tasks', { next: { revalidate: 30 } }),
fetch('https://api.example.com/activity/logs', { cache: 'no-store' }),
fetch('https://api.example.com/notifications', { next: { revalidate: 10 } }),
]).then(responses => Promise.all(responses.map(r => r.json())))
return <DashboardView {...{ users, projects, tasks, logs, notifs }} />
}
(3) النتائج
| البُعد | قبل التحسين | بعد التحسين |
|---|---|---|
| وقت تحميل الصفحة | 8 ثوانٍ (تسلسلي) | 2.1 ثانية (متوازي) |
| QPS قاعدة البيانات | 5,000 | 83 (تخزين مؤقت 60 ثانية) |
| شكاوى المستخدمين | 12 يوميًا | 0 |
| عدد أسطر الكود | 35 سطرًا (5 استدعاءات منفصلة) | 10 أسطر |
3. أنماط التخزين المؤقت الثلاثة لـ fetch
يوسع Next.js 16 واجهة fetch الخاصة بالويب بإضافة ثلاثة أنماط للتخزين المؤقت. جميع استدعاءات fetch في RSC تستخدم force-cache (تخزين مؤقت تلقائي) افتراضيًا، ما لم يُحدد نمط آخر بشكل صريح.
graph LR
A[RSC fetch] --> B{نمط التخزين المؤقت}
B --> C[force-cache<br/>القيمة الافتراضية]
B --> D[no-store<br/>جلب جديد مع كل طلب]
B --> E[revalidate:N<br/>نافذة زمنية]
C --> F[ذاكرة البيانات المؤقتة<br/>تخزين دائم]
D --> G[بيانات آنية<br/>بدون تخزين مؤقت]
E --> H[مخزنة مؤقتًا لمدة N ثانية<br/>إعادة الجلب بعد الانتهاء]
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#fff3cd
| النمط | الصيغة | السلوك | حالات الاستخدام |
|---|---|---|---|
force-cache (افتراضي) |
fetch(url) أو fetch(url, { cache: 'force-cache' }) |
يُسترد فقط أثناء البناء أو في أول طلب؛ النتائج تُخزن مؤقتًا بشكل دائم | بيانات نادرًا ما تتغير (مستندات، إعدادات ثابتة) |
no-store |
fetch(url, { cache: 'no-store' }) |
استرداد البيانات من جديد مع كل طلب؛ بدون تخزين مؤقت | بيانات آنية (معلومات المستخدم، المخزون) |
revalidate:N |
fetch(url, { next: { revalidate: 60 } }) |
مخزنة مؤقتًا لمدة 60 ثانية؛ عملية خلفية تشغّل تحديثًا عند الانتهاء | بيانات شبه آنية (أخبار، لوحات المتصدرين) |
(1) سلوك force-cache الافتراضي
إذا لم تُمرر أي خيارات، يقوم Next.js تلقائيًا بتخزين نتائج fetch مؤقتًا — الطلبات التي لها نفس URL والخيارات تُنفذ مرة واحدة فقط أثناء عملية البناء.
// app/products/page.tsx — force-cache افتراضي
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products').then(r => r.json())
// يُسترد مرة واحدة أثناء البناء، ويُستخدم التخزين المؤقت بعد ذلك
return <ProductList data={products} />
}
(2) no-store بيانات ديناميكية
// app/profile/page.tsx — استرداد أحدث البيانات مع كل طلب
export default async function ProfilePage() {
const user = await fetch('https://api.example.com/me', {
cache: 'no-store',
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
}).then(r => r.json())
return <ProfileView user={user} />
}
(3) revalidate نافذة زمنية
// app/blog/[slug]/page.tsx — تخزين مؤقت بنمط ISR
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 } // استخدام التخزين المؤقت لمدة ساعة واحدة
}).then(r => r.json())
return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}
▶ مثال: مقارنة أنماط التخزين المؤقت الثلاثة (مستوى الصعوبة: ⭐)
المخرجات:
يجلب البيانات ويُصيّر النتيجة.
// app/cache-demo/page.tsx
export default async function CacheDemoPage() {
const staticData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
cache: 'force-cache'
}).then(r => r.json())
const liveData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
cache: 'no-store'
}).then(r => r.json())
return (
<div>
<p>ثابت (force-cache): {staticData.datetime}</p>
<p>مباشر (no-store): {liveData.datetime}</p>
</div>
)
}
المخرجات:
ثابت (force-cache): 2026-07-06T10:00:00.000Z ← دائمًا نفس القيمة
مباشر (no-store): 2026-07-06T10:00:05.123Z ← يتغير مع كل تحديث
المخرجات:
المتصفح يُصيّر طابعين زمنيين:
ثابت (force-cache): 2026-07-06T10:00:00.000Z ← دائمًا نفس القيمة (مخزنة مؤقتًا عند البناء)
مباشر (no-store): 2026-07-06T10:00:05.123Z ← يتغير مع كل تحديث
4. إعادة التحقق عند الطلب: tags و revalidateTag
next: { tags: [...] } يوسم طلب fetch، ثم استخدم revalidateTag(tag) لتحديث التخزين المؤقت حسب الحاجة في Server Action أو Route Handler.
sequenceDiagram
participant A as Server Action
participant Cache as ذاكرة البيانات المؤقتة
participant DB as قاعدة البيانات
A->>DB: كتابة بيانات جديدة (إنشاء مهمة)
A->>Cache: revalidateTag('tasks')
Cache->>Cache: مسح جميع التخزين المؤقت المطابق للوسم
Note over Cache: في المرة القادمة fetch يُسترجع من جديد
| API | الغرض | مكان الاستدعاء |
|---|---|---|
next: { tags: ['tasks', 'projects'] } |
توسيم "fetch" | خيارات fetch() |
revalidateTag('tasks') |
مسح جميع التخزين المؤقت المرتبط بالوسم | Server Action / Route Handler |
revalidatePath('/dashboard') |
مسح التخزين المؤقت حسب المسار | Server Action / Route Handler |
▶ مثال: استخدام tags و revalidateTag (مستوى الصعوبة: ⭐⭐)
// app/tasks/data.ts — دوال استرجاع البيانات
export async function getTasks() {
return fetch('https://api.example.com/tasks', {
next: { tags: ['tasks'] }
}).then(r => r.json())
}
// app/tasks/actions.ts — Server Action: تحديث التخزين المؤقت بعد الكتابة
'use server'
import { revalidateTag } from 'next/cache'
export async function createTask(formData: FormData) {
const title = formData.get('title') as string
await fetch('https://api.example.com/tasks', {
method: 'POST',
body: JSON.stringify({ title, status: 'todo' })
})
revalidateTag('tasks') // مسح جميع مدخلات التخزين المؤقت لهذا الوسم
}
المخرجات:
يجلب البيانات ويُصيّر النتيجة.
▶ مثال: استخدام revalidatePath لمسح صفحة كاملة (مستوى الصعوبة: ⭐⭐)
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function publishArticle() {
await db.article.update({ where: { id: 1 }, data: { published: true } })
revalidatePath('/blog') // تحديث صفحة /blog
revalidatePath('/blog/[slug]') // تحديث جميع تفاصيل المقالات
}
المخرجات:
يُصيّر واجهة مكون publishArticle.
5. جلب البيانات المتوازي وتجنب تأثير الشلال (Waterfall)
نمط الشلال هو القاتل الأول للأداء — كل await ينتظر بالتسلسل اكتمال السابق. استخدام Promise.all() يتيح بدء جميع الطلبات في وقت واحد.
graph LR
subgraph "نمط الشلال (بطيء)"
A1[fetch A] --> A2[fetch B] --> A3[fetch C]
A1 -.- t1[2s]
A2 -.- t2[+2s = 4s]
A3 -.- t3[+2s = 6s]
end
subgraph "متوازي (سريع)"
B1[fetch A] -.- u1[2s]
C1[fetch B] -.- u2[2s]
D1[fetch C] -.- u3[2s]
B1 & C1 & D1 --> M[Promise.all<br/>الوقت الإجمالي ~2s]
end
| النمط | التنفيذ | الوقت الإجمالي (ثانيتان لكل منها) | السيناريوهات المناسبة |
|---|---|---|---|
| شلال تسلسلي | await A; await B; await C |
~6s | طلبات مترابطة |
| طلبات متوازية | Promise.all([A, B, C]) |
~2s | طلبات مستقلة غير مرتبطة |
| معالجة متوازية مرحلية | const a = await A; const [b, c] = await Promise.all([B(a.id), C]) |
~4s | طلبات مترابطة جزئيًا |
▶ مثال: التعرف على نمط الشلال التسلسلي (مستوى الصعوبة: ⭐)
// app/waterfall/page.tsx — ❌ شلال تسلسلي
export default async function WaterfallPage() {
const user = await fetch('https://api.example.com/user').then(r => r.json()) // 1s
const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json()) // انتظار حتى الانتهاء + 2s = 3s
const details = await Promise.all(tasks.map(t =>
fetch(`https://api.example.com/tasks/${t.id}/details`).then(r => r.json()) // انتظار حتى الانتهاء + 2s = 5s
))
return <div>الإجمالي: ~5s</div>
}
المخرجات:
يجلب البيانات ويُصيّر قائمة من العناصر.
النص المرئي: الإجمالي: ~5s
▶ مثال: التحسين المتوازي (مستوى الصعوبة: ⭐⭐)
المخرجات:
تُصيّر الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
// app/no-waterfall/page.tsx — ✅ تحسين متوازي
export default async function NoWaterfallPage() {
// المرحلة 1: استرجاع المستخدمين والبيانات الأولية بشكل متزامن
const [user, initialData] = await Promise.all([
fetch('https://api.example.com/user', { next: { revalidate: 10 } }).then(r => r.json()),
fetch('https://api.example.com/initial', { cache: 'no-store' }).then(r => r.json()),
])
// المرحلة 2: طلب يعتمد على user.id (لا يزال هناك شلال صغير، لكنه أفضل ما يمكن)
const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())
return <div>الإجمالي: ~2s (1s + 1s متوازي، ثم 1s)</div>
}
المخرجات:
يجلب البيانات ويُصيّر النتيجة.
النص المرئي: الإجمالي: ~2s (1s + 1s متوازي، ثم 1s)
▶ مثال: التحميل الكسول مع Suspense (مستوى الصعوبة: ⭐⭐⭐)
المخرجات:
تُصيّر الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
// app/suspense-demo/page.tsx — كل منطقة منفصلة تستخدم تغليف Suspense
import { Suspense } from 'react'
export default function SuspenseDemoPage() {
return (
<div>
<h1>لوحة التحكم</h1>
<Suspense fallback={<div>جاري تحميل الملف الشخصي...</div>}>
<ProfileSection />
</Suspense>
<Suspense fallback={<div>جاري تحميل المهام...</div>}>
<TaskSection />
</Suspense>
</div>
)
}
async function ProfileSection() {
const user = await fetch('https://api.example.com/user', { cache: 'no-store' }).then(r => r.json())
return <div>مرحبًا، {user.name}</div>
}
async function TaskSection() {
const tasks = await fetch('https://api.example.com/tasks', { next: { revalidate: 30 } }).then(r => r.json())
return <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
}
المخرجات:
يُصيّر هيكلًا ثابتًا فورًا، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
المحتوى الاحتياطي: جاري تحميل الملف الشخصي...
النص المرئي: لوحة التحكم | جاري تحميل الملف الشخصي... | }> | جاري تحميل المهام...
6. مثال كامل: لوحة تحكم محسّنة
// app/dashboard-optimized/page.tsx
import { Suspense } from 'react'
import { revalidateTag } from 'next/cache'
// ======== دوال البيانات ========
const API = 'https://jsonplaceholder.typicode.com'
async function getData<T>(endpoint: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${API}${endpoint}`, {
...options,
next: { tags: [endpoint.split('/')[1] ?? 'default'], ...(options as any)?.next },
})
if (!res.ok) throw new Error(`فشل في جلب ${endpoint}`)
return res.json()
}
// ======== استرجاع جميع البيانات بشكل متوازي ========
export default function DashboardOptimizedPage() {
return (
<div>
<h1>لوحة تحكم محسّنة</h1>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr 1fr' }}>
<Suspense fallback={<Skeleton label="المستخدمين" />}>
<DataCard title="المستخدمين" endpoint="/users" />
</Suspense>
<Suspense fallback={<Skeleton label="المنشورات" />}>
<DataCard title="المنشورات" endpoint="/posts" revalidate={120} />
</Suspense>
<Suspense fallback={<Skeleton label="التعليقات" />}>
<DataCard title="التعليقات" endpoint="/comments" />
</Suspense>
<Suspense fallback={<Skeleton label="المهام" />}>
<DataCard title="المهام" endpoint="/todos" revalidate={30} />
</Suspense>
</div>
</div>
)
}
async function DataCard({ title, endpoint, revalidate }: {
title: string
endpoint: string
revalidate?: number
}) {
const data = await getData<any[]>(endpoint, revalidate
? { next: { revalidate } }
: { cache: 'no-store' }
)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{title} <span style={{ fontSize: 14, color: '#666' }}>({data.length})</span></h2>
<ul>{data.slice(0, 5).map((item: any) => (
<li key={item.id}>{item.title ?? item.name ?? item.email}</li>
))}</ul>
</div>
)
}
function Skeleton({ label }: { label: string }) {
return <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, opacity: 0.5 }}>
جاري تحميل {label}...
</div>
}
// app/dashboard-optimized/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshSection(tag: string) {
revalidateTag(tag)
return { success: true }
}
❓ أسئلة شائعة
force-cache و no-store في fetch يتصرفان بنفس الطريقة في وضع التطوير ووضع الإنتاج؟npm run dev)، لا يزال force-cache يُجلب مع كل طلب (لتسهيل التصحيح). التخزين المؤقت لا يسري إلا في وضع الإنتاج (next start أو بعد البناء). هذا قرار تصميمي في Next.js — لجلب أحدث البيانات دائمًا أثناء مرحلة التطوير.revalidateTag و revalidatePath؟revalidateTag يمسح التخزين المؤقت حسب الوسم (لنفس البيانات عبر صفحات مختلفة)، بينما revalidatePath يمسح التخزين المؤقت حسب المسار (وصولاً إلى الصفحة أو نمط المسار). الأول مناسب للتحكم الدقيق في طبقة البيانات، بينما الثاني مناسب للتحديث على مستوى الصفحة. نوصي باستخدام revalidateTag كلما أمكن.cache أو next.revalidate مختلفة ستُعامل كمدخلات تخزين مؤقت مختلفة.Promise.all مع طلب فاشل؟Promise.all هي عملية "الكل أو لا شيء" — إذا فشل أي طلب فردي، يتم رفض الـ promise بالكامل. إذا كنت بحاجة إلى معالجة الأخطاء، قم بتغليف كل استدعاء fetch في Promise.allSettled أو كتلة try-catch. النمط الشائع هو const results = await Promise.all(urls.map(u => fetch(u).catch(() => null))).fetch في RSC؟fetch لا يحتوي على مهلة مدمجة. يمكنك تغليفه في AbortController: const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5000); fetch(url, { signal: ctrl.signal }). يُنصح بتغليف عميل fetch موحد على مستوى التطبيق.fetch في Next.js، مثل التخزين المؤقت التلقائي والوسوم (tags) وإعادة التحقق (revalidation). إذا استخدمت axios، ستحتاج إلى تنفيذ منطق التخزين المؤقت يدويًا أو تغليف طبقة متوافقة مع fetch حول axios. نوصي باستخدام طريقة fetch الأصلية كلما أمكن.📖 ملخص
- طريقة
fetchفي RSC تستخدمforce-cacheافتراضيًا، ويتم تخزين URLs المتطابقة مؤقتًا تلقائيًا cache: 'no-store'يعطل التخزين المؤقت؛ مناسب للبيانات الآنيةnext: { revalidate: N }ينفذ تخزينًا مؤقتًا بنافذة زمنية (مشابه لـ ISR)next: { tags: [...] }+revalidateTag()لتنفيذ تحديث التخزين المؤقت عند الطلبPromise.all()الطلبات المتوازية تمنع "تأثير الشلال" ويمكنها تقليل وقت التحميل بنسبة 60–80%- Suspense يستخدم تقسيم الحدود لتمكين التحميل بالتدفق (streaming)، فلا حاجة لانتظار جاهزية جميع البيانات
revalidatePath()يمسح التخزين المؤقت على مستوى الصفحة حسب المسار
📝 تمارين
-
تمرين أساسي (⭐): أنشئ
app/time-demo/page.tsx، واستخدمcache: 'no-store'وcache: 'force-cache'لإجراء طلبات منفصلة إلى World Time API، وقارن الفرق بين الطابعين الزمنيين، وتحقق من سلوك التخزين المؤقت. -
تمرين متقدم (⭐⭐): ابنِ
app/parallel-demo/page.tsxيستخدمPromise.allلجلب/usersو/postsو/comments(باستخدام JSONPlaceholder API)، وصيّر كل نقطة بيانات داخل مربع حدود<Suspense>منفصل لعرض تأثير التحميل بالتدفق. -
تحدٍّ (⭐⭐⭐): أنشئ صفحة قائمة مهام تدعم عمليات CRUD:
app/tasks/page.tsx(عرض قائمة المهام، باستخدام وسوم للتخزين المؤقت) وapp/tasks/actions.ts(استدعاءrevalidateTag('tasks')لتحديث القائمة بعد إضافة أو حذف مهمة). نفذ تحديثًا تفاؤليًا (optimistic update) لضمان تحديث القائمة فورًا بعد عملية الكتابة.