Next.js: التحميل التدفقي و Suspense
آخر تحديث: 2026-08-26
العرض البطيء يلغي حاجة المستخدمين للتحديق في شاشة فارغة أثناء الانتظار — يتم عرض الصفحة أثناء توليدها، مما يقلل زمن أول بايت (TTFB) بنسبة 60%.
1. ما ستتعلمه
- استراتيجية تقسيم حدود Suspense (الصفحة ← هيكل الشاشة ← المحتوى)
- اتفاقيات ملف
loading.tsxوالتغليف التلقائي لـ Suspense - تصميم ترتيب التحميل التدفقي و Fallback المتداخل لـ Suspense
- خطاف
use()في React 19: استهلاك Promises داخل المكونات streamTextمن AI SDK لتوليد النص بتأثير الآلة الكاتبة- وضع هجين يجمع بين العرض التدفقي والأغلفة الثابتة PPR
2. قصة حقيقية لمهندس واجهات أمامية
(1) نقطة الألم: شلال الطلبات يجعل تحميل الصفحة أبطأ بثلاث مرات
Charlie هو مهندس واجهات أمامية في فريق TaskFlow. لاحظ أن صفحة Dashboard تستغرق 4.2 ثانية للتحميل:
"الصفحة تحتوي على 5 بطاقات بيانات، وقائمة نشاط واحدة، ورسم بياني إحصائي واحد. يتم تحميل جميع البيانات بشكل متسلسل باستخدام
awaitداخل مكونات الصفحة؛ استدعاء API بطيء واحد يمكن أن يشل الصفحة بأكملها — تاركاً المستخدمين يحدقون في شاشة فارغة، منتظرين بلا جدوى."
| المشكلة | المدة | السبب |
|---|---|---|
| قائمة المستخدمين (200 مللي ثانية) + عدد العناصر (300 مللي ثانية) | 500 مللي ثانية | انتظار تسلسلي |
| الرسوم البيانية الإحصائية (800 مللي ثانية) | 800 مللي ثانية | معالجة خلفية بطيئة |
| سجل النشاط (400 مللي ثانية) | 400 مللي ثانية | استعلامات عبر الخدمات |
| إجمالي TTFP | 1,700 مللي ثانية | الكل تسلسلي |
(2) حل التحميل التدفقي + Suspense
تقسيم الصفحة إلى حدود Suspense متعددة، مع تحميل كل كتلة بيانات بشكل مستقل عبر التحميل التدفقي.
export default function DashboardPage() {
return (
<div>
<h1>نظرة عامة</h1>
<Suspense fallback={<SkeletonCards />}>
<UserCards />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<ActivityFeed />
</Suspense>
</div>
)
}
(3) العائد
| البعد | قبل التحسين (تسلسلي) | بعد التحسين (تدفقي) |
|---|---|---|
| زمن البايت الأول | 1,700 مللي ثانية | 50 مللي ثانية (غلاف ثابت) |
| تفاعلية الشاشة الأولى | 4.2 ثانية | 1.1 ثانية |
| حجب الكتل الكبيرة | الكل | ✅ لا شيء |
| تجربة المستخدم | شاشة فارغة لمدة 4 ثوان | هيكل الشاشة ← يمتلئ كتلة بكتلة |
3. استراتيجية تقسيم حدود Suspense
(1) طبقات التحميل الثلاث للصفحة
graph TB
A[الصفحة] --> B[الطبقة 1: الغلاف الثابت<br/>layout + header<br/>معاينة فورية]
A --> C[الطبقة 2: عرض الهيكل<br/>loading.tsx<br/>~200 مللي ثانية]
A --> D[الطبقة 3: المحتوى<br/>حدود Suspense<br/>وصول تدفقي كتلة بكتلة]
B --> E[يرى المستخدمون هيكل الصفحة]
C --> F[يرى المستخدمون رسماً متحركاً للعنصر النائب]
D --> G[يمتلئ المحتوى بالترتيب]
style B fill:#d4edda
style C fill:#fff3cd
style D fill:#cce5ff
| الطبقة | الآلية | وقت العرض | إدراك المستخدم |
|---|---|---|---|
| الغلاف الثابت | Layout | فوري | إطار الصفحة |
| هيكل الشاشة | loading.tsx |
~200 مللي ثانية | رسم متحرك للتحميل |
| المحتوى | Suspense + fallback | عرض كتلة بكتلة | تحميل تدريجي |
(2) تحميل البيانات التسلسلي مقابل المتوازي
// ❌ شلال تسلسلي — بطيء
export default async function SlowPage() {
const users = await fetch('https://api.example.com/users').then(r => r.json())
const projects = await fetch('https://api.example.com/projects').then(r => r.json())
const analytics = await fetch('https://api.example.com/analytics').then(r => r.json())
return <Dashboard users={users} projects={projects} analytics={analytics} />
}
// ✅ Suspense متوازي — سريع
export default function FastPage() {
return (
<div>
<Suspense fallback={<SkeletonCards />}><UserCards /></Suspense>
<Suspense fallback={<SkeletonCards />}><ProjectCards /></Suspense>
<Suspense fallback={<ChartSkeleton />}><AnalyticsChart /></Suspense>
</div>
)
}
▶ مثال: تنفيذ مكون Suspense
المخرجات:
يُعرض الغلاف الثابت فوراً، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
Fallback: }>
نص مرئي: }> | }> | }>
// components/UserCards.tsx — حد Suspense مستقل
export default async function UserCards() {
// محاكاة استعلامات بطيئة
const users = await new Promise<{ name: string; email: string }[]>((resolve) =>
setTimeout(() => resolve([
{ name: 'Alice', email: 'alice@taskflow.io' },
{ name: 'Bob', email: 'bob@taskflow.io' },
{ name: 'Charlie', email: 'charlie@taskflow.io' }
]), 2000)
)
return (
<div style={{ display: 'flex', gap: '1rem' }}>
{users.map((u) => (
<div key={u.email} style={{ border: '1px solid #ccc', padding: '1rem', borderRadius: 8 }}>
<h3>{u.name}</h3>
<p>{u.email}</p>
</div>
))}
</div>
)
}
المخرجات:
يُعرض الغلاف الثابت فوراً، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
// components/SkeletonCards.tsx — Fallback عرض الهيكل
export default function SkeletonCards() {
return (
<div style={{ display: 'flex', gap: '1rem' }}>
{[1, 2, 3].map((i) => (
<div key={i} style={{
width: 200, height: 100, borderRadius: 8,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite'
}} />
))}
</div>
)
}
4. loading.tsx و Suspense التلقائي
(1) أحكام الملف
| الملف | الغرض | نطاق Suspense |
|---|---|---|
app/dashboard/loading.tsx |
تغليف مسار الصفحة بالكامل | كل محتوى page.tsx |
app/dashboard/settings/loading.tsx |
مقطع settings فقط | settings/page.tsx |
graph TB
A[app/dashboard/] --> B[layout.tsx<br/>التخطيط الجذري]
A --> C[loading.tsx<br/>Suspense على مستوى الصفحة]
A --> D[page.tsx<br/>محتوى الصفحة]
D --> E{داخل الصفحة}
E --> F[<Suspense><SlowWidget/></Suspense>]
E --> G[<Suspense><AnotherWidget/></Suspense>]
C --> H[عرض fallback لـ loading.tsx]
D --> I[عرض محتوى page.tsx]
F --> J[تحميل تدفقي مستقل]
style C fill:#fff3cd
style F fill:#cce5ff
style G fill:#cce5ff
(2) تصميم loading.tsx
// app/dashboard/loading.tsx — هيكل الشاشة على مستوى الصفحة
export default function DashboardLoading() {
return (
<div style={{ padding: '2rem' }}>
<div style={{ height: 32, width: 200, background: '#eee', borderRadius: 4, marginBottom: '2rem' }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
{[1, 2, 3].map((i) => (
<div key={i} style={{ height: 120, background: '#f5f5f5', borderRadius: 8 }} />
))}
</div>
<div style={{ height: 300, background: '#f5f5f5', borderRadius: 8, marginTop: '2rem' }} />
</div>
)
}
▶ مثال: هيكل loading.tsx المتداخل
app/dashboard/ ← loading.tsx للصفحة الكاملة
├── layout.tsx ← شريط التنقل (معاينة فورية)
├── loading.tsx ← هيكل الشاشة للصفحة
├── page.tsx ← محتوى Dashboard
├── projects/ ← مسار فرعي
│ ├── loading.tsx ← هيكل الشاشة لقائمة المشاريع فقط
│ └── page.tsx ← قائمة المشاريع
└── settings/
└── loading.tsx ← هيكل الشاشة للإعدادات
// app/dashboard/projects/loading.tsx — حالة التحميل لقائمة المشاريع فقط
export default function ProjectsLoading() {
return (
<div>
{[1, 2, 3, 4].map((i) => (
<div key={i} style={{
height: 64, marginBottom: 8, borderRadius: 6,
background: 'linear-gradient(90deg, #e8e8e8 0%, #f5f5f5 50%, #e8e8e8 100%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s ease-in-out infinite'
}} />
))}
</div>
)
}
5. تصميم Suspense المتداخل و Fallback
(1) استراتيجيات التداخل
graph TB
A[الصفحة] --> B[الطبقة الخارجية Suspense<br/>fallback: هيكل الصفحة]
A --> C[الطبقة الداخلية Suspense 1<br/>fallback: قالب البطاقة]
A --> D[الطبقة الداخلية Suspense 2<br/>fallback: إطار الرسم البياني]
D --> E[مستوى أعمق Suspense<br/>fallback: إطار مصغر]
B -->|عرض فوري| F[هيكل الصفحة]
C -->|~500 مللي ثانية| G[بطاقة المستخدم]
D -->|~800 مللي ثانية| H[الرسوم البيانية الإحصائية]
E -->|~1200 مللي ثانية| I[تفاصيل الرسم البياني]
style B fill:#f8d7da
style C fill:#fff3cd
style D fill:#cce5ff
style E fill:#d4edda
| مستوى التداخل | Fallback الموصى به | وقت العرض | كثافة المعلومات |
|---|---|---|---|
| الطبقة الخارجية (الصفحة) | عنصر نائب كبير | فوري | منخفضة (الهيكل) |
| الطبقة الوسطى (المكون) | هيكل بشكل المكون | ~500 مللي ثانية | متوسطة (المخطط) |
| الطبقة الداخلية (التفاصيل) | عنصر نائب صغير + رسم متحرك مصغر | ~1200 مللي ثانية | عالية (المحتوى) |
▶ مثال: Suspense متداخل ثلاثي المستويات
المخرجات:
رسم تخطيطي لـ Suspense المتداخل: fallback خارجي (غلاف الصفحة) + fallback داخلي (تحميل القسم).
// app/analytics/page.tsx — تطبيق عملي لـ Suspense المتداخل
import { Suspense } from 'react'
function SummarySkeleton() { return <div style={{ height: 100, background: '#eee' }} /> }
function ChartSkeleton() { return <div style={{ height: 300, background: '#f5f5f5' }} /> }
function DetailSkeleton() { return <div style={{ height: 60, background: '#fafafa' }} /> }
export default function AnalyticsPage() {
return (
<div>
<h1>تقرير التحليل</h1>
{/* الطبقة الخارجية: بطاقة النظرة العامة */}
<Suspense fallback={<SummarySkeleton />}>
<SummaryCards />
</Suspense>
{/* الطبقة الوسطى: الرسوم البيانية */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
{/* الطبقة الداخلية: قائمة التفاصيل */}
<Suspense fallback={<DetailSkeleton />}>
<TopProjects />
</Suspense>
</div>
)
}
async function SummaryCards() {
await new Promise((r) => setTimeout(r, 500))
return <div>الدخل لهذا الشهر: $120,000 • عدد المستخدمين: 15,230 • المشاريع: 342</div>
}
async function RevenueChart() {
await new Promise((r) => setTimeout(r, 1000))
return <div style={{ height: 300, background: '#e8f4f8' }}>[رسم بياني: اتجاهات الإيرادات الشهرية]</div>
}
async function TopProjects() {
await new Promise((r) => setTimeout(r, 1500))
return <div>أفضل مشروع: TaskFlow (45%), WebApp (30%), Mobile (25%)</div>
}
المخرجات:
يُعرض الغلاف الثابت فوراً. تعرض الأقسام الديناميكية fallback "جار التحميل..." حتى يتم تحميل البيانات.
المحتوى المرئي: تقرير التحليل | }> | }>
6. خطاف use() في React 19 لقراءة Promises
مقارنة بين use() و await
| الميزة | await (مكون الخادم) |
use() (مكون العميل) |
|---|---|---|
| الموقع | مكون الخادم فقط | مكون العميل (بما في ذلك 'use client') |
| سلوك الحجب | حجب عرض المكون | رمي Promise ← Suspense يلتقطه |
| توقيع النوع | const data = await promise |
const data = use(promise) |
| إعادة التقديم | تلقائي (إعادة تنفيذ RSC) | يجب تشغيله يدوياً |
// ✅ مكون الخادم: await
async function ServerProfile({ id }: { id: string }) {
const user = await fetch(`https://api.example.com/users/${id}`).then(r => r.json())
return <div>{user.name}</div>
}
// ✅ مكون العميل: use()
'use client'
import { use } from 'react'
function ClientProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise)
return <div>{user.name}</div>
}
(2) استخدام use() مع التحميل التدفقي في مكون العميل
// components/StreamingProfile.tsx — use() + Suspense
'use client'
import { use } from 'react'
interface User { name: string; email: string; bio: string }
function UserProfile({ promise }: { promise: Promise<User> }) {
const user = use(promise)
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.bio}</p>
</div>
)
}
// الاستخدام في الصفحة
import { Suspense } from 'react'
export default function ProfilePage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params) // params أيضاً Promise
const userPromise = fetch(`https://api.example.com/users/${id}`).then(r => r.json())
return (
<Suspense fallback={<div>جار تحميل ملف المستخدم...</div>}>
<UserProfile promise={userPromise} />
</Suspense>
)
}
params و searchParams هما Promises؛ يجب فكهما باستخدام await (RSC) أو use() (العميل).
▶ مثال: use() لتنفيذ تغذية تمرير لا نهائي
المخرجات:
يُعرض الغلاف الثابت فوراً، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
// components/InfinitePosts.tsx
'use client'
import { use, useState, useTransition } from 'react'
interface Post { id: number; title: string }
async function fetchPosts(page: number): Promise<Post[]> {
const res = await fetch(`/api/posts?page=${page}&limit=10`)
return res.json()
}
export default function InfinitePosts({ initialPromise }: { initialPromise: Promise<Post[]> }) {
const [page, setPage] = useState(1)
const [postsPromise, setPostsPromise] = useState(initialPromise)
const [isPending, startTransition] = useTransition()
const posts = use(postsPromise)
const loadMore = () => {
startTransition(() => {
setPage((p) => p + 1)
setPostsPromise(fetchPosts(page + 1))
})
}
return (
<div>
{posts.map((post) => <div key={post.id}>{post.title}</div>)}
<button onClick={loadMore} disabled={isPending}>
{isPending ? 'جار التحميل......' : 'تحميل المزيد'}
</button>
</div>
)
}
المخرجات:
يُعرض: صفحة InfinitePosts مع عناصر واجهة تفاعلية.
7. دمج AI SDK streamText
(1) بنية التحميل التدفقي لـ streamText
graph LR
A[رسائل المستخدم] --> B[معالج المسار<br/>POST /api/chat]
B --> C[AI SDK streamText]
C --> D[مزود LLM<br/>OpenAI / Anthropic]
D -->|رمز تدفقي| E[ReadableStream]
E --> F[مكون العميل<br/>useChat Hook]
F --> G[تأثير الآلة الكاتبة]
style B fill:#cce5ff
style C fill:#d4edda
style F fill:#fff3cd
| المكون | الوظيفة | التثبيت |
|---|---|---|
المكتبة الأساسية ai |
دوال streamText |
npm install ai |
@ai-sdk/openai |
مزود OpenAI | npm install @ai-sdk/openai |
useChat |
خطاف العميل | مضمن في حزمة ai |
(2) التوجيه التدفقي من جانب الخادم
// app/api/chat/route.ts — واجهة محادثة AI مباشرة
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o'),
system: 'أنت مساعد AI لـ TaskFlow. أجب عن الأسئلة المتعلقة بإدارة المشاريع.',
messages
})
return result.toDataStreamResponse()
}
▶ مثال: تأثير الآلة الكاتبة من جانب العميل
المخرجات:
تم تنفيذ كود TypeScript بنجاح.
// components/ChatBox.tsx
'use client'
import { useChat } from 'ai/react'
export default function ChatBox() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return (
<div style={{ maxWidth: 600, margin: '0 auto' }}>
<div style={{ height: 400, overflowY: 'auto', border: '1px solid #ccc', padding: '1rem' }}>
{messages.map((m) => (
<div key={m.id} style={{
textAlign: m.role === 'user' ? 'right' : 'left',
marginBottom: '1rem'
}}>
<strong>{m.role === 'user' ? 'أنت' : 'AI'}:</strong>
<p>{m.content}</p>
</div>
))}
{isLoading && <p>AI يكتب......</p>}
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', marginTop: '1rem' }}>
<input
value={input}
onChange={handleInputChange}
placeholder="أدخل سؤالك..."
style={{ flex: 1, padding: '0.5rem' }}
/>
<button type="submit" disabled={isLoading} style={{ padding: '0.5rem 1rem' }}>
إرسال
</button>
</form>
</div>
)
}
المخرجات:
يُعرض قائمة بتمرير الرسائل، مع عرض كل m.
useChat يستخدم افتراضياً POST إلى /api/chat. لتحديد نقطة نهاية API مخصصة، مرر خيار api: useChat({ api: '/api/ai/chat' }).
8. مثال كامل: لوحة تحليلات AI + بيانات تدفقية
// app/dashboard/page.tsx — لوحة تحكم TaskFlow + تحليل AI
import { Suspense } from 'react'
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
// وحدات عرض الهيكل
function MetricSkeleton() {
return <div style={{ height: 100, background: '#f0f0f0', borderRadius: 8 }} />
}
function ChartSkeleton() {
return <div style={{ height: 300, background: '#f5f5f5', borderRadius: 8 }} />
}
// مكون بيانات بطيء
async function TeamMetrics() {
const metrics = await new Promise<{ members: number; projects: number; tasks: number }>(
(resolve) => setTimeout(() => resolve({ members: 12, projects: 45, tasks: 230 }), 1500)
)
return (
<div style={{ display: 'flex', gap: '1rem' }}>
<div>👥 {metrics.members} أعضاء</div>
<div>📁 {metrics.projects} مشروع</div>
<div>✅ {metrics.tasks} مهمة</div>
</div>
)
}
async function ActivityChart() {
const data = await new Promise<number[]>((r) => setTimeout(() => r([30, 45, 78, 92, 55, 88, 120]), 2000))
return (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '0.5rem', height: 200 }}>
{data.map((v, i) => (
<div key={i} style={{ height: v, width: 40, background: '#4f46e5', borderRadius: '4px 4px 0 0' }} />
))}
</div>
)
}
export default async function DashboardPage() {
const session = await auth()
if (!session) redirect('/login')
return (
<div>
<h1>نظرة عامة على TaskFlow</h1>
<p>مرحباً بعودتك، {session.user!.name}</p>
<Suspense fallback={<MetricSkeleton />}>
<TeamMetrics />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<ActivityChart />
</Suspense>
</div>
)
}
// app/api/chat/route.ts — مساعد تحليل AI
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o-mini'),
system: 'أنت مساعد AI لإدارة المشاريع. استناداً إلى بيانات المشروع المقدمة، قدم تحليلاً وتوصيات. اجعل إجاباتك موجزة.',
messages
})
return result.toDataStreamResponse()
}
// components/ChatPanel.tsx
'use client'
import { useChat } from 'ai/react'
export default function ChatPanel() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return (
<div style={{ position: 'fixed', bottom: 0, right: 20, width: 380, border: '1px solid #ccc', borderRadius: '8px 8px 0 0' }}>
<div style={{ padding: '0.5rem 1rem', background: '#4f46e5', color: '#fff', borderRadius: '8px 8px 0 0' }}>
مساعد تحليل AI
</div>
<div style={{ height: 300, overflowY: 'auto', padding: '0.5rem' }}>
{messages.map((m) => (
<div key={m.id} style={{ marginBottom: '0.5rem' }}>
<strong>{m.role === 'user' ? 'أنا' : 'AI'}:</strong>
<p style={{ margin: 0 }}>{m.content}</p>
</div>
))}
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', borderTop: '1px solid #eee' }}>
<input value={input} onChange={handleInputChange} placeholder="اسأل سؤالاً عن المشروع..." style={{ flex: 1, padding: '0.5rem', border: 'none' }} />
<button type="submit" disabled={isLoading} style={{ padding: '0.5rem 1rem', background: '#4f46e5', color: '#fff', border: 'none' }}>إرسال</button>
</form>
</div>
)
}
❓ أسئلة شائعة
loading.tsx هو اتفاقية ملف تنشئ حد Suspense تلقائياً للصفحة بأكملها، مما يجعل التنفيذ بسيطاً. مكونات <Suspense> تُستخدم للتحكم الدقيق داخل الصفحة؛ يمكنها تغليف أي عدد من كتل البيانات المستقلة للتحميل التدفقي المتوازي.use() أفضل من await؟use() يمكن استخدامه في مكونات العميل، مما يسمح للمكون نفسه بإعلان اعتماده على Promise، مع أقرب حد Suspense يتولى حالة التحميل. مناسب للسيناريوهات التي تحتاج إلى تشغيل عمليات غير متزامنة من الواجهة الأمامية (مثل النقر على "تحميل المزيد").streamText من AI SDK واستدعاء OpenAI API مباشرة؟streamText يتعامل تلقائياً مع بروتوكول SSE (Server-Sent Events)، والتحكم في الضغط الخلفي، وعد الرموز، وإعادة محاولة الأخطاء. استدعاء OpenAI API مباشرة يتطلب معالجة يدوية لـ ReadableStream وتنسيق الاستجابة.<Suspense>. PPR يولد الأجزاء الثابتة مسبقاً، والأجزاء الديناميكية تُعرض فورياً — الاثنان يتكاملان بشكل مثالي.📖 ملخص
- Suspense يقسم الصفحة إلى كتل تدفقية مستقلة، مما يمنع الطلبات التسلسلية من حجب الصفحة بأكملها
- ملف
loading.tsxينشئ تلقائياً Suspense لصفحة المسار، مع fallback يعرض هيكل الشاشة - استراتيجية Suspense المتداخل: هيكل خارجي (فوري) ← مخطط وسطي (~500 مللي ثانية) ← تفاصيل داخلية (~1200 مللي ثانية)
- خطاف
use()في React 19 يستهلك Promises في مكونات العميل، مستخدماً مع Suspense streamTextمن AI SDK +useChatللمحادثات التدفقية بأسلوب الآلة الكاتبة- دمج العرض التدفقي مع الأغلفة الثابتة PPR: عرض مسبق للأجزاء الثابتة + تدفق الأجزاء الديناميكية
📝 تمارين
-
تمرين أساسي (⭐): نفذ ثلاثة حدود Suspense في صفحة واحدة لتحميل قائمة المستخدمين، وإحصائيات المشاريع، وسجل النشاط على التوالي، مع تأخير مختلف لكل حد (500 مللي ثانية / 1000 مللي ثانية / 1500 مللي ثانية).
-
تمرين متقدم (⭐⭐): استخدم
streamTextمن AI SDK لإنشاء مسار API لمساعد ترجمة، واستخدمuseChatمن جانب العميل لتنفيذ تأثير الآلة الكاتبة الذي يعرض نتائج الترجمة حرفاً بحرف. -
تحد (⭐⭐⭐): نفذ صفحة Dashboard متعددة المستويات المتداخلة: الطبقة الخارجية
loading.tsxتعرض هيكل الشاشة الكامل، مع ثلاث طبقات متداخلة من Suspense داخل الصفحة (بطاقة الإحصائيات ← الرسم البياني ← القائمة التفصيلية). الطبقة الخارجية تستخدم خطافuse()لتحميل المزيد من البيانات عند النقر.