Next.js: التحميل التدفقي و Suspense

آخر تحديث: 2026-08-26

العرض البطيء يلغي حاجة المستخدمين للتحديق في شاشة فارغة أثناء الانتظار — يتم عرض الصفحة أثناء توليدها، مما يقلل زمن أول بايت (TTFB) بنسبة 60%.

1. ما ستتعلمه



2. قصة حقيقية لمهندس واجهات أمامية

(1) نقطة الألم: شلال الطلبات يجعل تحميل الصفحة أبطأ بثلاث مرات

Charlie هو مهندس واجهات أمامية في فريق TaskFlow. لاحظ أن صفحة Dashboard تستغرق 4.2 ثانية للتحميل:

"الصفحة تحتوي على 5 بطاقات بيانات، وقائمة نشاط واحدة، ورسم بياني إحصائي واحد. يتم تحميل جميع البيانات بشكل متسلسل باستخدام await داخل مكونات الصفحة؛ استدعاء API بطيء واحد يمكن أن يشل الصفحة بأكملها — تاركاً المستخدمين يحدقون في شاشة فارغة، منتظرين بلا جدوى."

المشكلة المدة السبب
قائمة المستخدمين (200 مللي ثانية) + عدد العناصر (300 مللي ثانية) 500 مللي ثانية انتظار تسلسلي
الرسوم البيانية الإحصائية (800 مللي ثانية) 800 مللي ثانية معالجة خلفية بطيئة
سجل النشاط (400 مللي ثانية) 400 مللي ثانية استعلامات عبر الخدمات
إجمالي TTFP 1,700 مللي ثانية الكل تسلسلي

(2) حل التحميل التدفقي + Suspense

تقسيم الصفحة إلى حدود Suspense متعددة، مع تحميل كل كتلة بيانات بشكل مستقل عبر التحميل التدفقي.

TSX
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) طبقات التحميل الثلاث للصفحة

100%
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) تحميل البيانات التسلسلي مقابل المتوازي

TSX
// ❌ شلال تسلسلي — بطيء
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

المخرجات:

TEXT 📖 للعرض فقط
يُعرض الغلاف الثابت فوراً، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
Fallback: }>
نص مرئي: }> | }> | }>
TSX
// 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>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
يُعرض الغلاف الثابت فوراً، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
TSX
// 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
100%
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

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 المتداخل

TEXT 📖 للعرض فقط
app/dashboard/              ← loading.tsx للصفحة الكاملة
├── layout.tsx               ← شريط التنقل (معاينة فورية)
├── loading.tsx              ← هيكل الشاشة للصفحة
├── page.tsx                 ← محتوى Dashboard
├── projects/                ← مسار فرعي
│   ├── loading.tsx          ← هيكل الشاشة لقائمة المشاريع فقط
│   └── page.tsx             ← قائمة المشاريع
└── settings/
    └── loading.tsx          ← هيكل الشاشة للإعدادات
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) استراتيجيات التداخل

100%
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 متداخل ثلاثي المستويات

المخرجات:

TEXT 📖 للعرض فقط
رسم تخطيطي لـ Suspense المتداخل: fallback خارجي (غلاف الصفحة) + fallback داخلي (تحميل القسم).
TSX
// 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>
}

المخرجات:

TEXT 📖 للعرض فقط
يُعرض الغلاف الثابت فوراً. تعرض الأقسام الديناميكية fallback "جار التحميل..." حتى يتم تحميل البيانات.
المحتوى المرئي: تقرير التحليل | }> | }>


6. خطاف use() في React 19 لقراءة Promises

مقارنة بين use() و await

الميزة await (مكون الخادم) use() (مكون العميل)
الموقع مكون الخادم فقط مكون العميل (بما في ذلك 'use client')
سلوك الحجب حجب عرض المكون رمي Promise ← Suspense يلتقطه
توقيع النوع const data = await promise const data = use(promise)
إعادة التقديم تلقائي (إعادة تنفيذ RSC) يجب تشغيله يدوياً
TSX
// ✅ مكون الخادم: 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() مع التحميل التدفقي في مكون العميل

TSX
// 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>
  )
}
💡 نصيحة: في Next.js 16، كل من params و searchParams هما Promises؛ يجب فكهما باستخدام await (RSC) أو use() (العميل).

▶ مثال: use() لتنفيذ تغذية تمرير لا نهائي

المخرجات:

TEXT 📖 للعرض فقط
يُعرض الغلاف الثابت فوراً، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
TSX
// 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>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
يُعرض: صفحة InfinitePosts مع عناصر واجهة تفاعلية.


7. دمج AI SDK streamText

(1) بنية التحميل التدفقي لـ streamText

100%
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) التوجيه التدفقي من جانب الخادم

TS
// 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()
}

▶ مثال: تأثير الآلة الكاتبة من جانب العميل

المخرجات:

TEXT 📖 للعرض فقط
تم تنفيذ كود TypeScript بنجاح.
TSX
// 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>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
يُعرض قائمة بتمرير الرسائل، مع عرض كل m.
🔥 خطأ شائع: useChat يستخدم افتراضياً POST إلى /api/chat. لتحديد نقطة نهاية API مخصصة، مرر خيار api: useChat({ api: '/api/ai/chat' }).



8. مثال كامل: لوحة تحليلات AI + بيانات تدفقية

TSX
// 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>
  )
}
TS
// 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()
}
TSX
// 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>
  )
}
💻 وصف التأثير: تعرض الصفحة أولاً عرضاً هيكلياً ← بعد 0.5 ثانية، تمتلئ بطاقات المؤشرات ← بعد ثانية واحدة، يظهر الرسم البياني الشريطي ← تظهر لوحة AI على اليمين للمحادثة الفورية، مع عرض المحتوى كلمة بكلمة بشكل تدفقي.


❓ أسئلة شائعة

س ما الفرق بين Suspense و loading.tsx؟
ج loading.tsx هو اتفاقية ملف تنشئ حد Suspense تلقائياً للصفحة بأكملها، مما يجعل التنفيذ بسيطاً. مكونات <Suspense> تُستخدم للتحكم الدقيق داخل الصفحة؛ يمكنها تغليف أي عدد من كتل البيانات المستقلة للتحميل التدفقي المتوازي.
س هل يؤثر العرض التدفقي على SEO؟
ج لا. زواحف محركات البحث (Googlebot) تنتظر حتى يكتمل HTML النهائي قبل فهرسته؛ المحتوى التدفقي يُحمل تدريجياً وليس محقوناً لاحقاً. حمولة RSC في Next.js تضمن أن الزواحف ترى المحتوى الكامل.
س متى يكون use() أفضل من await؟
ج use() يمكن استخدامه في مكونات العميل، مما يسمح للمكون نفسه بإعلان اعتماده على Promise، مع أقرب حد Suspense يتولى حالة التحميل. مناسب للسيناريوهات التي تحتاج إلى تشغيل عمليات غير متزامنة من الواجهة الأمامية (مثل النقر على "تحميل المزيد").
س ما الفرق بين استخدام streamText من AI SDK واستدعاء OpenAI API مباشرة؟
ج streamText يتعامل تلقائياً مع بروتوكول SSE (Server-Sent Events)، والتحكم في الضغط الخلفي، وعد الرموز، وإعادة محاولة الأخطاء. استدعاء OpenAI API مباشرة يتطلب معالجة يدوية لـ ReadableStream وتنسيق الاستجابة.
س كيف يعمل العرض التدفقي و PPR (Partial Prerendering) معاً؟
ج الغلاف الثابت في PPR يتكون من التخطيط الخارجي والمحتوى الثابت، بينما الأجزاء الديناميكية الداخلية مغلفة في <Suspense>. PPR يولد الأجزاء الثابتة مسبقاً، والأجزاء الديناميكية تُعرض فورياً — الاثنان يتكاملان بشكل مثالي.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): نفذ ثلاثة حدود Suspense في صفحة واحدة لتحميل قائمة المستخدمين، وإحصائيات المشاريع، وسجل النشاط على التوالي، مع تأخير مختلف لكل حد (500 مللي ثانية / 1000 مللي ثانية / 1500 مللي ثانية).

  2. تمرين متقدم (⭐⭐): استخدم streamText من AI SDK لإنشاء مسار API لمساعد ترجمة، واستخدم useChat من جانب العميل لتنفيذ تأثير الآلة الكاتبة الذي يعرض نتائج الترجمة حرفاً بحرف.

  3. تحد (⭐⭐⭐): نفذ صفحة Dashboard متعددة المستويات المتداخلة: الطبقة الخارجية loading.tsx تعرض هيكل الشاشة الكامل، مع ثلاث طبقات متداخلة من Suspense داخل الصفحة (بطاقة الإحصائيات ← الرسم البياني ← القائمة التفصيلية). الطبقة الخارجية تستخدم خطاف use() لتحميل المزيد من البيانات عند النقر.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%