Next.js: النموذج الذهني لمكونات الخادم (Server Components)

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

النموذج الذهني لـ RSC هو أهم تحول نموذجي في Next.js 16 — فقط بفهمه يمكنك استيعاب فلسفة التصميم وراء App Router.

1. ما ستتعلمه



2. قصة حقيقية لمطور Full-Stack

(1) نقطة الألم: حجم الحزمة (Bundle Size) خارج السيطرة

Alice هي مطورة Full-Stack في فريق TaskFlow. صفحة Dashboard التي بنتها تتضمن مكون جدول بيانات يستخدم ثلاث مكتبات — date-fns وrecharts وlodash — فقط لتنسيق التواريخ ورسم بعض المخططات الشريطية. تضخمت حزمة JS الأولية للصفحة إلى 480 KB، وانخفضت درجة Lighthouse Performance إلى 52. والأسوأ من ذلك، أن هذا الجدول مجرد مكون تقديمي يُصيّر على الخادم — لا يحتاج المستخدمون للتفاعل معه إطلاقًا — ومع ذلك لا يزال كود هذه المكتبات يُنزّل إلى المتصفح.

(2) الحل عبر RSC

RSC يضمن أن مكونات الخادم تعمل فقط على الخادم، وتُخرج HTML خالصًا وبيانات متسلسلة، مع استبعاد حزم JavaScript تمامًا.

TSX
// app/dashboard/page.tsx — Server Component (Zero-Client JS)
import { getSalesData } from '@/lib/db'
import { formatDistanceToNow } from 'date-fns'

export default async function DashboardPage() {
  const sales = await getSalesData()  // الوصول المباشر إلى قاعدة البيانات
  return (
    <div>
      <h1>Dashboard — {sales.length} سجل</h1>
      <SalesTable data={sales} />
    </div>
  )
}

async function SalesTable({ data }: { data: Sale[] }) {
  return (
    <table>
      {data.map(row => (
        <tr key={row.id}>
          <td>{formatDistanceToNow(row.createdAt)}</td>
          <td>{row.amount}</td>
        </tr>
      ))}
    </table>
  )
}

(3) النتائج

البُعد Pure Client Component RSC
حجم الحزمة (Bundle Size) 480 KB (يتضمن date-fns + ReCharts + Lodash) 0 KB (مكتبات الخادم لا تُنزّل)
الوصول إلى قاعدة البيانات يتطلب وسيط API Route وصول مباشر (زمن انتقال صفري)
تصيير المحتوى الظاهر أولًا (Above-the-fold) يتطلب تنزيل JS + تنفيذه HTML فوري
SEO يعتمد على SSR / التصيير من جانب العميل دعم أصلي
درجة Lighthouse 52 96


3. التعريف الأساسي لـ RSC

RSC (React Server Component) هو نوع مكونات جديد قُدّم في React 19، يعمل فقط على الخادم ولا يُرسل أبدًا إلى متصفح العميل. كود RSC (بما في ذلك المكتبات التابعة) لا يظهر في حزمة JS، لذا يمكنك استخدام مكتبات كبيرة بأمان، والوصول مباشرة إلى قواعد البيانات، وقراءة نظام الملفات.

100%
graph TB
    subgraph "جانب الخادم (Server)"
        A[RSC Components] --> B[قاعدة البيانات/نظام الملفات/API]
        A --> C[تسلسل إلى RSC Payload<br/>بروتوكول React Flight]
    end
    subgraph "العميل (Browser)"
        D[RSC Payload] --> E[Client Component<br/>الحفاظ على منطق التفاعل]
        D --> F[تصيير HTML خالص<br/>صفر تكلفة JS]
    end
    C --> D
    style A fill:#d4edda
    style D fill:#cce5ff
الخاصية Server Component Client Component
بيئة التشغيل جانب الخادم (Node.js) المتصفح
حزمة JS ❌ غير مضمنة ✅ مضمنة
قاعدة البيانات/نظام الملفات ✅ وصول مباشر ❌ غير متاح (يتطلب API)
React Hooks (useState/useEffect) ❌ غير متاحة ✅ متاحة
معالجة الأحداث (onClick/onSubmit) ❌ غير متاحة ✅ متاحة
Async/Await ✅ دعم أصلي ❌ يتطلب معالجة إضافية

(1) معنى "Zero-Client JS"

المبدأ الأساسي لـ RSC هو: إذا كان المكون لا يحتوي على منطق تفاعلي، فلا ينبغي إرسال كوده إلى المتصفح. هذا يعني:

(2) RSC مقابل SSR التقليدي

SSR التقليدي (Page Router) يُصيّر أيضًا HTML على الخادم، لكنه لا يزال يرسل كود JavaScript الخاص بالمكون إلى العميل من أجل hydration. أما RSC فمختلف تمامًا — كود مكونات الخادم لا يصل أبدًا إلى العميل.

البُعد SSR التقليدي (Pages Router) RSC (App Router)
التصيير من جانب الخادم ✅ HTML ✅ HTML
Client Hydration ✅ كامل ❌ غير مطلوب
إرسال كود المكون إلى العميل ✅ الكل ❌ Client Component فقط
الحفاظ على الحالة (State) يتطلب معالجة دقيقة بلا حالة بطبيعته (Stateless)
توقيت استرجاع البيانات getServerSideProps مباشرة داخل المكون await

▶ مثال: التحقق من فروق حجم الحزمة (مستوى الصعوبة: ⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
الرسم التخطيطي: RSC Components؛ قاعدة البيانات/نظام الملفات/API؛ تسلسل إلى RSC Payload بروتوكول React Flight؛ RSC Payload؛ Client Component الحفاظ على منطق التفاعل؛ تصيير HTML خالص صفر تكلفة JS.
TSX
// app/bundle-demo/page.tsx — Server Component (خادم بحت)
import { format, addDays } from 'date-fns'

export default function BundleDemoPage() {
  const today = new Date()
  const dates = Array.from({ length: 7 }, (_, i) => {
    const d = addDays(today, i)
    return { label: format(d, 'EEEE'), date: format(d, 'yyyy-MM-dd') }
  })

  return (
    <div>
      <h1>هذا الأسبوع</h1>
      <ul>{dates.map(d => <li key={d.date}>{d.label}: {d.date}</li>)}</ul>
    </div>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
فحص ما بعد البناء .next/static/chunks — لا يتضمن كود date-fns

المخرجات:

TEXT 📖 للعرض فقط
بعد البناء، لا يحتوي مجلد .next/static/chunks على كود date-fns — تم تأكيد صفر تكلفة JS من جانب العميل. الصفحة تُصيّر: عنوان "هذا الأسبوع" + 7 عناصر قائمة (مثل "الاثنين: 2026-07-06"، "الثلاثاء: 2026-07-07"، ...).


4. توجيه 'use client' وحدود العميل (Client Boundaries)

'use client' هو توجيه على مستوى الوحدة (module-level) يعلّم المكون في الملف كمكون عميل (Client Component). عندما تكون هناك حاجة لوظائف تفاعلية (useState، onClick، useEffect) في شجرة مكونات RSC، يجب إضافة هذا التوجيه في أعلى الملف.

100%
graph TB
    A[Root Layout<br/>Server Component] --> B[NavBar<br/>Server Component]
    A --> C[DashboardPage<br/>Server Component]
    C --> D[SalesChart<br/>'use client']
    C --> E[DataTable<br/>Server Component]
    D --> F[منطق التفاعل<br/>useState / useEffect]

    style A fill:#d4edda
    style B fill:#d4edda
    style C fill:#d4edda
    style D fill:#cce5ff
    style E fill:#d4edda
التوجيه الوظيفة مثال
'use client' تعليم الوحدة كمكون عميل (Client Component) 'use client'; export default function Btn() { ... }
'use server' تعليم الدالة كـ Server Action 'use server'; export async function create() { ... }

(1) قواعد اختراق الحدود

هناك قاعدتان صارمتان في شجرة مكونات RSC:

TSX
// ✅ صحيح: Server Component يستورد Client Component
// app/page.tsx (Server)
import ClientCounter from './ClientCounter'
export default function Page() {
  return <ClientCounter />
}

// app/ClientCounter.tsx (Client)
'use client'
import { useState } from 'react'
export default function ClientCounter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
TSX
// ❌ خطأ: Client Component لا يمكنه استيراد Server Component مباشرة
// app/ClientList.tsx
'use client'
import ServerItem from './ServerItem'  // ❌ خطأ ترجمة: Server Component لا يمكن استيراده من جانب العميل

export default function ClientList() {
  return <ServerItem />  // هذا السطر سيسبب خطأ.
}

(2) كيفية تضمين Server Component داخل Client Component

نصيحة: مرر البيانات عبر children props — فتحة children في مكون العميل يمكنها استقبال نتيجة التصيير من مكون الخادم.

TSX
// app/layout.tsx (Server)
import ClientShell from './ClientShell'
import ServerSidebar from './ServerSidebar'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <ClientShell sidebar={<ServerSidebar />}>
      {children}
    </ClientShell>
  )
}

▶ مثال: الطريقة الصحيحة للتعامل مع تمرير Props — Children Props (مستوى الصعوبة: ⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
يتضمن شريطًا جانبيًا.
النص المرئي: }>
      {children}
TSX
// app/interleaving/ClientWrapper.tsx
'use client'
import { useState } from 'react'

export default function ClientWrapper({ sidebar, children }: {
  sidebar: React.ReactNode
  children: React.ReactNode
}) {
  const [isOpen, setIsOpen] = useState(true)
  return (
    <div style={{ display: 'flex' }}>
      {isOpen && <aside>{sidebar}</aside>}
      <button onClick={() => setIsOpen(!isOpen)}>تبديل</button>
      <main>{children}</main>
    </div>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
مكون تفاعلي مع إدارة حالة.
النص المرئي: }
TSX
// app/interleaving/page.tsx (Server)
import ClientWrapper from './ClientWrapper'
import { getSidebarData } from '@/lib/db'

export default function InterleavingPage() {
  const items = getSidebarData() // استرجاع البيانات على جانب الخادم
  return (
    <ClientWrapper sidebar={<ServerItemList items={items} />}>
      <h1>المحتوى الرئيسي</h1>
    </ClientWrapper>
  )
}

async function ServerItemList({ items }: { items: string[] }) {
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>
}

▶ مثال: قيود Props القابلة للتسلسل (مستوى الصعوبة: ⭐⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر قائمة من العناصر باستخدام .map().
النص المرئي: }> | المحتوى الرئيسي
TSX
// app/serializable/page.tsx (Server)
function greet() { return 'hello' }  // ❌ الدوال غير قابلة للتسلسل
const date = new Date()              // ⚠️ Date غير مسموح به كـ RSC Props

// app/serializable/ClientComponent.tsx
'use client'
export default function ClientComponent(props: {
  fn: () => string       // ❌ الدوال كـ props → خطأ وقت التشغيل
  date: Date             // ⚠️ Date → يتم تحويله إلى نص قد يفقد المنطقة الزمنية
  data: { name: string } // ✅ الكائنات العادية مسموحة
}) {
  return <div>{props.data.name}</div>
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر واجهة مكون ClientComponent.


5. دمج React Flight Payload مع شجرة المكونات

بعد اكتمال تصيير RSC من جانب الخادم، يُخرج تنسيق بيانات خاص يسمى RSC Payload (بروتوكول React Flight)، والذي يحتوي على شجرة HTML المتسلسلة، ومراجع المكونات، وبيانات props. عندما يستقبل العميل الـ payload، يدمجه مع مكون العميل المحلي لتشكيل شجرة المكونات النهائية.

(1) هيكل RSC Payload

100%
sequenceDiagram
    participant Server as خادم Next.js
    participant Client as المتصفح

    Server->>Server: تنفيذ شجرة مكونات RSC
    Server->>Server: تسلسل إلى React Flight Payload
    Server->>Client: إرسال RSC Payload + HTML
    Client->>Client: تحليل Flight Payload
    Client->>Client: دمج Client Component (Hydrate)
    Client->>Client: تصيير الواجهة النهائية
المكون الوصف مثال
مخرجات Server Component جزء HTML متسلسل <div><h1>Dashboard</h1></div>
مرجع Client Component معرف الوحدة + Props {id: "./chart.js", props: {data: [...]}}
مرجع البيانات نتائج استعلام قاعدة البيانات {sales: [{id:1, amount: 100}]}
التدفق (Stream) تقسيم حدود Suspense إرسال أجزاء متعددة بشكل تدريجي

▶ مثال: عرض RSC Payload (مستوى الصعوبة: ⭐⭐)

اعرض استجابة RSC في لوحة Network بأدوات المطور في المتصفح:

BASH
# افتح لوحة Network، قم بتحديث الصفحة، فلتر Fetch/XHR
# ابحث عن طلب الصفحة الحالية، اعرض Response
# Content-Type: text/x-component أي RSC Payload

المخرجات:

TEXT 📖 للعرض فقط
# مقتطف من RSC Payload (مبسط):
M1:{"id":"./app/page.tsx","chunks":["app/page-abc123.js"]}
J0:["$","div",null,{"children":["$","h1",null,{"children":"Dashboard"}]}]
S1:"react.suspense"

▶ مثال: كيف يشير Client Component إلى Server Component (مستوى الصعوبة: ⭐⭐⭐)

TSX
// app/flight-demo/ServerData.tsx — مكون بيانات خادم بحت
export default async function ServerData() {
  const data = await fetch('https://api.example.com/data').then(r => r.json())
  return <pre>{JSON.stringify(data, null, 2)}</pre>
}
TSX
// app/flight-demo/ClientShell.tsx
'use client'
export default function ClientShell({ dataSlot }: { dataSlot: React.ReactNode }) {
  return (
    <div className="card">
      <h2>غلاف العميل (Client Shell)</h2>
      <div className="server-data">{dataSlot}</div>
    </div>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر: غلاف العميل (Client Shell)
النص المرئي: Client Shell
TSX
// app/flight-demo/page.tsx
import ClientShell from './ClientShell'
import ServerData from './ServerData'

export default function FlightDemoPage() {
  return (
    <ClientShell dataSlot={<ServerData />}>
  )
}


6. مثال كامل: بنية شجرة مكونات RSC

TSX
// app/rsc-architecture/layout.tsx — RSC Layout
import ClientShell from './ClientShell'
import { getUser } from '@/lib/auth'

export default async function RscLayout({ children }: { children: React.ReactNode }) {
  const user = await getUser()                    // ✅ استعلام مباشر لقاعدة البيانات
  return (
    <ClientShell username={user?.name ?? 'زائر'}>
      <nav>
        <a href="/">الرئيسية</a>
        <a href="/dashboard">لوحة التحكم</a>
        <a href="/settings">الإعدادات</a>
      </nav>
      {children}
    </ClientShell>
  )
}

// app/rsc-architecture/ClientShell.tsx
'use client'
import { useState } from 'react'
import type { ReactNode } from 'react'

export default function ClientShell({ username, children }: {
  username: string
  children: ReactNode
}) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  return (
    <div data-theme={theme}>
      <header>
        <span>مرحبًا، {username}</span>
        <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
          تبديل {theme}
        </button>
      </header>
      {children}
    </div>
  )
}

// app/rsc-architecture/page.tsx
import { getProjects } from '@/lib/db'

export default async function RscArchitecturePage() {
  const projects = await getProjects()
  return (
    <div>
      <h1>المشاريع ({projects.length})</h1>
      <table>
        <thead><tr><th>الاسم</th><th>الحالة</th></tr></thead>
        <tbody>
          {projects.map(p => (
            <tr key={p.id}>
              <td>{p.name}</td>
              <td><StatusBadge status={p.status} /></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

function StatusBadge({ status }: { status: string }) {
  const colors: Record<string, string> = {
    active: '#4caf50', archived: '#9e9e9e', draft: '#ff9800'
  }
  return <span style={{ background: colors[status] ?? '#ccc', padding: '2px 8px', borderRadius: 4 }}>{status}</span>
}


❓ أسئلة شائعة

س هل RSC و SSR نفس الشيء؟
ج لا. في SSR التقليدي، لا يزال كود JavaScript يُرسل إلى العميل بعد تصيير HTML على الخادم (hydration). أما في RSC، كود Server Component لا يُرسل أبدًا إلى العميل — صفر تكلفة JavaScript. يمكن أن يتعايش SSR و RSC معًا (الوضع الافتراضي لـ App Router هو مزيج من RSC و SSR).
س هل جميع المكونات في ملف 'use client' هي مكونات عميل؟
ج نعم. 'use client' هو توجيه على مستوى الوحدة — جميع المكونات المُصدّرة من الملف هي مكونات عميل. يُنصح بتقسيم المكونات التفاعلية إلى ملفات منفصلة لتقليل كمية كود العميل.
س لماذا لا يمكن لـ Client Component استيراد Server Component مباشرة؟
ج لأن Server Components موجودة فقط في بيئة تشغيل الخادم. عندما يعمل Client Component في المتصفح، كود Server Component غير موجود أصلًا. الطريقة الصحيحة هي الربط بينهما باستخدام children prop أو Server Actions.
س ماذا يحدث إذا تم تمرير دالة كـ prop إلى مكون عميل؟
ج سيتم إلقاء خطأ. RSC Payload مبني على تسلسل JSON (بروتوكول React Flight)، والدوال لا يمكن تسلسلها. إذا كنت بحاجة إلى تمرير callback، استخدم Server Actions أو نمط معالج الأحداث (event handler).
س كيف تحدد ما إذا كان المكون يجب أن يكون Server أم Client؟
ج أبسط قاعدة: إذا كان المكون يحتاج إلى تفاعلية (useState، useEffect، onClick، واجهات برمجة المتصفح)، فهو Client؛ وإلا، استخدم Server Component افتراضيًا. استراتيجية التحسين الشائعة هي استخراج الأجزاء التفاعلية إلى غلاف Client صغير، مع إبقاء الجسم الرئيسي كـ Server Component.
س ما العلاقة بين RSC Payload و HTML؟
ج Next.js 16 يرسل كلاً من HTML (للتحميل الفوري) و RSC Payload (لإعادة بناء شجرة المكونات). HTML يضمن عرض الشاشة الأولى فورًا، بينما RSC Payload يتولى التفاعلات بعد تحليله على العميل. معًا يحققان "شاشة أولى فورية + تفاعلية كاملة".

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ Server Component في app/page.tsx يستدعي مباشرة fetch('https://api.github.com/repos/vercel/next.js') لاسترجاع البيانات وعرض عدد النجوم (star rating). تحقق من أن العميل لا يُنزّل مكتبات مثل node-fetch.

  2. تمرين متقدم (⭐⭐): أنشئ صفحة تحتوي على Client Component (زر عداد) و Server Component (قائمة مستخدمين)، ومرر بيانات الخادم إلى غلاف العميل باستخدام children prop. تحقق من استجابة RSC Payload في تبويب Network بأدوات مطور المتصفح.

  3. تحدٍّ (⭐⭐⭐): ابنِ شجرة مكونات من ثلاث طبقات: Layout (Server) → ClientTabs (Client، يحتوي على useState لإدارة التبويب الحالي) → ServerTabContent (Server Components تُمرر عبر children)، حيث يكون محتوى كل تبويب استعلام بيانات غير متزامن مختلف. تأكد من أن جلب البيانات من جميع Server Components لا يزيد من حجم حزمة العميل.

Web-Tutorial.com

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

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

100%