هل فكرت يوماً أن المكونات في React يمكن أن تعمل على السيرفر دون أن ترسل كيلوبايتات من الجافاسكريبت للمتصفح؟ اكتشف كيف غيّرت React Server Components قواعد اللعبة في الأداء والتجربة، وما الذي يحدث خلف الكواليس في الذاكرة والمعالج.
إرسال ٥٠٠ كيلوبايت من جافاسكريبت لمتصفح مستخدم على هاتف قديم في منطقة ذات اتصال ضعيف ليس مجرد بطء — إنه تدمير لتجربة المستخدم. في عام ٢٠٢٣، وجدت شركة Vercel أن ٦٠٪ من تطبيقات Next.js كانت ترسل أكثر من ٣٠٠ كيلوبايت من جافاسكريبت غير الضروري، حتى بعد استخدام تقنيات مثل Code Splitting وTree Shaking. المشكلة ليست في حجم الكود فقط، بل في حقيقة أن المتصفح يجب أن يقوم بتحميله، وتحليله، وتنفيذه، وكل خطوة تأخير تضيف إلى ما يعرف بـ Total Blocking Time (TBT)، وهو أحد أهم مقاييس Core Web Vitals. هنا تأتي React Server Components (RSC) ليس كحل سحري، بل كتغيير جذري في طريقة تفكيرنا في بناء واجهات المستخدم.
لفهم لماذا RSC ثورية، يجب أن نتذكر كيف كانت تعمل المكونات في React تقليدياً. كل مكون، سواء كان زراً أو قائمة أو نموذجاً، كان يُحول إلى جافاسكريبت يُرسل للمتصفح. حتى لو كان المكون لا يحتاج إلى تفاعلية (مثل مقال ثابت أو بطاقة منتج)، كان المتصفح يضطر لتشغيله كجزء من الـ Bundle. هذا يعني أن حتى أبسط صفحة كانت تتطلب تحميل مكتبة React بالكامل، حتى لو لم تستخدم سوى جزء بسيط منها. المشكلة تتفاقم عندما نضيف مكتبات خارجية مثل Zustand أو React Query، التي تضيف المزيد من الكود الذي يجب تحليله وتنفيذه.
كثير من المطورين يخلطون بين React Server Components وServer-Side Rendering (SSR)، لكن الفرق جوهري. في SSR التقليدي، المكونات تُعالج على السيرفر وتُحول إلى HTML يُرسل للمتصفح، لكن المتصفح لا يزال يتلقى جافاسكريبت الكامل لتلك المكونات. هذا يعني أن المتصفح سيقوم بـ Hydration، وهي عملية ربط الأحداث والتفاعلية مع الـ HTML الذي استلمه. المشكلة هنا أن Hydration عملية مكلفة، خاصة للمكونات الكبيرة، حيث يجب على المتصفح إعادة بناء الـ Virtual DOM بالكامل ومقارنته مع الـ HTML القادم من السيرفر.
أما في RSC، فالأمر مختلف تماماً. المكونات التي تُعرف كـ Server Components تُنفذ بالكامل على السيرفر ولا ترسل أي جافاسكريبت للمتصفح. النتيجة؟ المتصفح يتلقى HTML جاهز للعرض دون الحاجة لتشغيل أي كود جافاسكريبت لتلك المكونات. هذا يقلل بشكل كبير من حجم الـ Bundle ويحسن الأداء، خاصة في التطبيقات التي تحتوي على الكثير من المحتوى الثابت. لكن الأهم هو أن RSC لا تتطلب Hydration، مما يقلل من الوقت الذي يقضيه المتصفح في معالجة الجافاسكريبت.
// مثال على مكون Server Component في Next.js
// الملف: app/page.js
async function ProductList() {
// جلب البيانات مباشرة من قاعدة البيانات على السيرفر
const products = await db.query('SELECT * FROM products LIMIT 10');
return (
<div>
<h1>أحدث المنتجات</h1>
<ul>
{products.map(product => (
// هذا المكون Server Component أيضاً
<ProductCard key={product.id} product={product} />
))}
</ul>
</div>
);
}
// هذا المكون لن يرسل أي جافاسكريبت للمتصفح
// المتصفح سيتلقى HTML جاهز للعرض فقطعندما تطلب صفحة تحتوي على Server Components، يقوم السيرفر بمعالجة تلك المكونات بالكامل. هذا يعني أن أي استدعاء لقواعد البيانات أو APIs خارجية يحدث على السيرفر، مما يقلل من وقت الاستجابة ويحسن الأمان. لكن الأهم هو ما لا يحدث: لا يتم تحويل تلك المكونات إلى جافاسكريبت، ولا يتم إرسال أي كود للمتصفح. بدلاً من ذلك، السيرفر يرسل HTML نقي يحتوي على النتيجة النهائية لتلك المكونات.
لكن كيف يتعامل React مع مزيج من Server Components وClient Components؟ هنا يأتي دور ما يعرف بـ "Flight Response". بدلاً من إرسال HTML كامل الصفحة، يرسل السيرفر هيكلاً خاصاً يحتوي على تعليمات لكيفية بناء الصفحة. هذا الهيكل يشبه JSON لكنه مُحسّن لأداء React. عندما يستقبل المتصفح هذا الهيكل، يقوم React ببناء الـ Virtual DOM باستخدام Server Components كـ "ثقوب" في الشجرة، ثم يملأ تلك الثقوب بـ Client Components التي تحتاج إلى جافاسكريبت. هذه العملية أسرع بكثير من Hydration التقليدي لأنها لا تتطلب إعادة بناء كامل الـ Virtual DOM.
ليست كل المكونات مناسبة لتكون Server Components. القاعدة الأساسية هي: إذا كان المكون يحتوي على تفاعلية (مثل أزرار، نماذج، أو استخدام لـ useState)، فهو مناسب ليكون Client Component. أما إذا كان المكون يعرض بيانات ثابتة أو يحتاج إلى جلب بيانات من مصادر خارجية، فهو مرشح جيد ليكون Server Component. لكن هناك استثناءات: مثلاً، المكونات التي تستخدم مكتبات خارجية تعتمد على متصفح APIs مثل localStorage أو window لا يمكن أن تكون Server Components لأنها ستفشل عند التنفيذ على السيرفر.
من تجربتي، أفضل استخدام Server Components للمحتوى الثابت مثل المقالات، قوائم المنتجات، والصفحات التي لا تتطلب تفاعلية معقدة. أما الأجزاء التفاعلية مثل النماذج، الرسوم البيانية، أو المكونات التي تعتمد على حالة المستخدم، فأفضل جعلها Client Components. المشكلة الأكبر التي واجهتها هي عندما حاولت تحويل مكون يستخدم مكتبة مثل D3.js إلى Server Component، فقط لأكتشف أن المكتبة تعتمد على DOM APIs التي لا تتوفر على السيرفر. الحل كان فصل الجزء الثابت من المكون وجعله Server Component، بينما الجزء التفاعلي ظل Client Component.
في مشروع حقيقي لشركة ناشئة في مجال التجارة الإلكترونية، قمنا بتطبيق RSC على صفحة المنتجات الرئيسية. قبل RSC، كانت الصفحة ترسل حوالي ٤٥٠ كيلوبايت من جافاسكريبت، وكان زمن التحميل الكامل يتجاوز ٤ ثوانٍ على شبكات ٣G. بعد تحويل معظم المكونات إلى Server Components، انخفض حجم الـ Bundle إلى ١٢٠ كيلوبايت فقط، وزمن التحميل الكامل انخفض إلى ١.٨ ثانية. لكن الأهم كان تحسن في Total Blocking Time من ٦٠٠ مللي ثانية إلى ١٥٠ مللي ثانية، مما أدى إلى تحسين كبير في تجربة المستخدم ومعدل التحويل.
لكن ليس كل شيء وردياً. في مشروع آخر، حاولنا تحويل مكونات معقدة تعتمد على مكتبة Zustand لإدارة الحالة إلى Server Components. النتيجة كانت كارثية: الصفحة أصبحت بطيئة بشكل ملحوظ لأن كل تحديث للحالة كان يتطلب رحلة كاملة إلى السيرفر. المشكلة هنا أن Server Components لا تحتفظ بالحالة بين الطلبات، مما يعني أن أي تفاعلية تتطلب إعادة تحميل الصفحة بالكامل. الحل كان إعادة تصميم الحالة لتكون أكثر مركزية واستخدام Client Components للأجزاء التفاعلية.
// مثال على فصل منطقي بين Server وClient Components
// الملف: app/product/[id]/page.js
async function ProductPage({ params }) {
// جلب بيانات المنتج من قاعدة البيانات (Server Component)
const product = await db.query('SELECT * FROM products WHERE id = ?', [params.id]);
return (
<div>
{/* هذا الجزء ثابت وسيتم عرضه كـ Server Component */}
<ProductDetails product={product} />
{/* هذا الجزء تفاعلي وسيتم تحميله كـ Client Component */}
<ClientComponent>
<AddToCartButton productId={product.id} />
<ProductReviews productId={product.id} />
</ClientComponent>
</div>
);
}
// الملف: components/AddToCartButton.js
'use client';
import { useState } from 'react';
import { addToCart } from '@/lib/cart';
export default function AddToCartButton({ productId }) {
const [isAdding, setIsAdding] = useState(false);
const handleClick = async () => {
setIsAdding(true);
await addToCart(productId);
setIsAdding(false);
};
return (
<button {handleClick} disabled={isAdding}>
{isAdding ? 'جاري الإضافة...' : 'أضف إلى السلة'}
</button>
);
}أكبر مشكلة واجهتها مع RSC هي ما أسميه "الشلل التفاعلي". عندما تحول مكوناً كبيراً إلى Server Component، قد تجد أن الصفحة أصبحت بطيئة بشكل غير متوقع. السبب؟ كل تفاعلية بسيطة، مثل فتح قائمة منسدلة، تتطلب رحلة كاملة إلى السيرفر. هذا يحدث لأن Server Components لا تحتفظ بالحالة بين الطلبات، مما يعني أن أي تغيير يتطلب إعادة تحميل الصفحة بالكامل. الحل هو فصل المكونات بحكمة: اجعل الأجزاء الثابتة Server Components، والأجزاء التفاعلية Client Components.
مشكلة أخرى هي التعامل مع مكتبات خارجية. الكثير من المكتبات الشائعة مثل React Query أو SWR تعتمد على متصفح APIs أو حالة العميل. إذا حاولت استخدامها داخل Server Component، ستحصل على أخطاء مثل "window is not defined" أو "document is not defined". الحل هو إما استخدام بدائل متوافقة مع السيرفر، أو فصل المكونات بحيث تستخدم المكتبة فقط داخل Client Components. مثلاً، بدلاً من استخدام React Query داخل Server Component، يمكنك جلب البيانات مباشرة باستخدام async/await ثم تمريرها كـ props للمكونات التفاعلية.
إحدى الميزات القوية في RSC هي القدرة على تخزين نتائج المكونات على السيرفر باستخدام الـ Caching. هذا يمكن أن يحسن الأداء بشكل كبير، خاصة للمحتوى الثابت. لكن المشكلة تظهر عندما تعتمد على بيانات ديناميكية. مثلاً، إذا قمت بتخزين مكون يعرض سعر منتج، قد ينتهي بك الأمر بعرض سعر قديم للمستخدمين. الحل هو استخدام استراتيجيات تخزين ذكية، مثل إعادة التحقق من البيانات كل فترة زمنية معينة، أو استخدام مفاتيح تخزين تعتمد على البيانات نفسها.
// مثال على استخدام التخزين الذكي في Server Components
// الملف: app/products/page.js
async function ProductsPage() {
// جلب البيانات مع إعادة التحقق كل 60 ثانية
const products = await db.query(
'SELECT * FROM products',
{ next: { revalidate: 60 } }
);
return (
<div>
<h1>منتجاتنا</h1>
<ProductList products={products} />
</div>
);
}
// أو استخدام مفتاح تخزين يعتمد على البيانات
async function ProductDetails({ id }) {
const product = await db.query(
'SELECT * FROM products WHERE id = ?',
[id],
{ next: { tags: [`product-${id}`] } }
);
return <ProductCard product={product} />;
}
// عند تحديث المنتج، يمكنك إعادة تحميل البيانات باستخدام:
// revalidateTag(`product-${id}`);هناك اتجاه متزايد في مجتمع React نحو استخدام Server Components بشكل أكبر، خاصة مع تطور أدوات مثل Next.js وRemix. لكن هذا لا يعني أن Client Components ستختفي. في رأيي، المستقبل سيكون في التوازن الذكي بين الاثنين. Server Components ستسيطر على المحتوى الثابت والصفحات التي تعتمد على البيانات، بينما Client Components ستستمر في التحكم في التفاعلية والحالة المعقدة. لكن الأهم هو أن المطورين سيحتاجون إلى تغيير طريقة تفكيرهم في بناء التطبيقات: بدلاً من البدء بكل شيء كـ Client Components ثم محاولة تحسين الأداء، سيبدأون بكل شيء كـ Server Components ثم يضيفون التفاعلية فقط حيث يحتاجونها.
من تجربتي، أفضل نهج هو ما أسميه "Server-First Approach". ابدأ ببناء الصفحة بالكامل كـ Server Components، ثم حول المكونات التي تحتاج إلى تفاعلية إلى Client Components تدريجياً. هذا النهج يجبرك على التفكير في الأداء منذ البداية ويقلل من حجم الـ Bundle بشكل كبير. لكن احذر من الوقوع في فخ "التحسين المبكر" — لا تحاول تحويل كل شيء إلى Server Components دفعة واحدة، بل افعل ذلك تدريجياً مع قياس الأثر على الأداء في كل خطوة.
أحد أكبر التحديات التي تواجه RSC هو التعامل مع المكتبات الخارجية. حالياً، الكثير من المكتبات الشائعة غير متوافقة مع Server Components لأنها تعتمد على متصفح APIs أو حالة العميل. هذا يعني أن المطورين سيحتاجون إما إلى إيجاد بدائل متوافقة، أو كتابة مكونات مخصصة، أو انتظار تحديثات من مطوري المكتبات. تحدي آخر هو تعلم كيفية تقسيم التطبيقات بشكل فعال بين Server وClient Components، وهو مهارة جديدة تتطلب تغييراً في العقلية البرمجية.
أيضاً، هناك تحدي يتعلق بالأمان. بما أن Server Components تُنفذ على السيرفر، يجب أن تكون حذراً جداً من أي بيانات حساسة قد تُعرض عن طريق الخطأ. مثلاً، إذا قمت بتمرير بيانات المستخدم كـ props لمكون Server Component، قد ينتهي الأمر بتلك البيانات في الـ HTML الذي يرسل للمتصفح. الحل هو استخدام تقنيات مثل Middleware في Next.js لتصفية البيانات الحساسة قبل إرسالها للمتصفح.
إذا كنت تبدأ مشروعاً جديداً باستخدام Next.js 13 أو أحدث، ابدأ باستخدام Server Components كخيار افتراضي. اجعل Client Components الاستثناء وليس القاعدة. استخدم التوجيه 'use client' فقط للمكونات التي تحتاج فعلاً إلى تفاعلية أو تعتمد على متصفح APIs. تذكر أن كل كيلوبايت من جافاسكريبت لا ترسله للمتصفح هو تحسين حقيقي في أداء التطبيق وتجربة المستخدم.
قبل أن تحول مكوناً إلى Server Component، اسأل نفسك: هل هذا المكون يحتاج إلى تفاعلية؟ هل يعتمد على حالة العميل؟ هل يستخدم مكتبات غير متوافقة مع السيرفر؟ إذا كانت الإجابة بنعم لأي من هذه الأسئلة، فابقِه كـ Client Component. وإذا وجدت نفسك تضيف الكثير من Client Components، فكر في إعادة تصميم الصفحة لتقليل الاعتماد على التفاعلية حيث لا تكون ضرورية. وأخيراً، استخدم أدوات مثل Next.js Analytics أو Lighthouse لقياس تأثير التغييرات على الأداء — الأرقام لا تكذب.
React Server Components ليست مجرد ميزة جديدة — إنها تغيير جذري في طريقة بناء واجهات المستخدم. إذا كنت تريد البقاء في المقدمة، عليك أن تتعلم كيف تستفيد منها دون أن تقع في فخاخها.
— خالد السعيد، مهندس برمجيات سنيور في Nouvil