العرض في جانب العميل
العرض في جانب العميل (Client-side rendering) يرسل مستند HTML شبه فارغ. فبرنامج بوتستراب صغير ينزّل شيفرة JavaScript ويفرّها وينفّذها، وهي تبني DOM وتجلب البيانات وتربط التوجيه (routing) وترسم كل بكسل يراه المستخدم. الدور الوحيد للخادم (server) هو تسليم هيكل HTML وشيفرة JavaScript اللازمة لتشغيل التطبيق في المتصفح.
هذا النموذج هو ما دفع ظهور تطبيقات الصفحة الواحدة (single-page applications). جعل تطبيقات الويب تبدو كبرمجيات مثبّتة — انتقالات فورية بين المسارات، وتحديثات متفائلة (optimistic updates)، وتفاعل غني دون أن تُحمَّل الصفحة من جديد أبدًا. لكن الثمن هو أن أول رسم يعتمد كليًا على سرعة قدرة العميل (client) على تنزيل الحزمة (bundle) وتنفيذها.
لا يزال العرض في جانب العميل نمطًا مفيدًا اليوم، لكن موقعه قد ضاق. ما زال ملائمًا بقوة للوحات التحكم التي تتطلب مصادقة، والأدوات الداخلية، والأدوات المضمّنة (embedded widgets). ولم يعد الخيار الافتراضي للمواقع الغنية بالمحتوى أو الموجّهة للجمهور، فمقايضة أول تحميل فيها مرتفعة جدًا مقارنةً بما تقدّمه الأطر الحديثة جاهزًا.
متى يناسب العرض في جانب العميل ومتى لا يناسب
يناسب العرض في جانب العميل جيدًا عندما:
- تكون الصفحة خلف نظام مصادقة (فهرسة محركات البحث ليست هدفًا).
- يفتح المستخدم التطبيق مرة واحدة ويبقى فيه — فعمق الجلسة الطويل يوزّع كلفة الحزمة.
- يكون التفاعل كثيفًا وذا حالة (أدوات الرسم، وجداول البيانات، وبيئات التطوير المتكاملة، ولوحات التصميم).
- يحتاج التطبيق إلى دعم العمل دون اتصال عبر عامل خدمة (service worker) وهيكل تطبيق.
لا يناسب العرض في جانب العميل عندما:
- يجب أن تفهرس محركات البحث الصفحة أو تُشارك عبر معاينات الروابط.
- يصل المستخدم جديدًا ويغادر بسرعة (صفحات تسويقية، ومنشورات مدونة، وأخبار).
- يكون زمن أكبر عنصر محتوى مرئي (LCP) وزمن أول بايت (TTFB) مقاييس أعمال حرجة.
- يكون الجمهور على أجهزة منخفضة المواصفات أو شبكات بطيئة حيث تهيمن كلفة تحليل وتنفيذ JavaScript.
ولكل ما ورد في المجموعة الثانية، يتفوق النموذج الهجين — العرض من الخادم أو التوليد الثابت للاستجابة الأولية، ثم الترطيب (hydration) في جانب العميل — على العرض في جانب العميل المجاني في كل مقياس أساسي للأداء يهم.
إعداد أدنى حديث للعرض في جانب العميل
إليك أصغر مثال ذا معنى للعرض في جانب العميل باستخدام واجهة React 18+ الحالية. أُهملت ReactDOM.render في React 18 واستُبدلت فعليًا بـ createRoot، الذي يفعّل لشجرة المكوّنات (component tree) لديك العرض المتزامن (Concurrent Rendering).
main.jsx · App.jsx · index.html
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>
);
يستلم المتصفح عنصرًا فارغًا ``، وينزّل main.jsx (وما يستورده)، ثم تُركّب React الشجرة. كل بايت من الواجهة والبيانات والسلوك ينتقل عبر الشبكة كشيفرة JavaScript.
الشكل الأدائي لصفحة تعمل بالعرض في جانب العميل
ينقل العرض في جانب العميل العملَ من الخادم إلى العميل. وهذا يزيح منحنى الأداء بشكل يمكن التنبؤ به:
| المقياس | سلوك العرض في جانب العميل |
|---|---|
| TTFB | سريع — يعيد الخادم هيكلًا ثابتًا. |
| FCP | متأخر — محجوب على تنزيل الحزمة وتحليلها وأول عرض. |
| LCP | متأخر — يعتمد عادةً على أكبر طلب بيانات داخل التطبيق. |
| TTI / INP | متأخر في البداية، ثم منخفض جدًا — لا رحلة ذهاب وإياب لكل تفاعل. |
| CLS | خطر — الانتقال من هيكل فارغ إلى واجهة ممتلئة هو إزاحة تخطيط في انتظار أن تحدث ما لم تحجز مساحة. |
كل من TTI وFCP يتأخران تقريبًا بشكل خطي مع حجم الحزمة وكلفة تحليلها. وعلى هاتف Android من الفئة المتوسطة، تضيف كل 100 كيلوبايت من JavaScript مضغوطة تأخيرًا ملحوظًا قبل الرسم.
مقايضات تستحق التسمية
تحسين محركات البحث (SEO). تنفّذ زواحف البحث الآن شيفرة JavaScript، لكنها تفعل ذلك ضمن طابور وميزانية. قد تُفهرَس صفحة تحتاج إلى ثلاثة طلبات متتابعة قبل أن يوجد محتواها الرئيسي ببطء، أو تُفهرَس جزئيًا، أو تعود معاينات روابطها فارغة. إذا كان الظهور في نتائج البحث مهمًا، فلا تعتمد على العرض في جانب العميل وحده.
الأداء المُدرَك (Perceived performance). يحدّق المستخدم في شاشة فارغة — أو الأسوأ، في مؤشر دوران فوق شاشة فارغة — طوال مدة تنزيل الحزمة زائدًا أول جلب للبيانات. وكلما طالت السلسلة، بدت الصفحة أكثر عطبًا.
تباين الأجهزة (Device variance). يستطيع حاسوب MacBook حديث تحليل ميغابايت من JS خلال عشرات الأجزاء من الثانية. وقد يستغرق هاتف Android عمره ثلاث سنوات أكثر من ثانية بكثير على الحزمة نفسها. يضع العرض في جانب العميل وحدة المعالجة (CPU) الخاصة بالمستخدم على المسار الحرج؛ أما العرض من الخادم والتوليد الثابت فيضعان مسار الخادم هناك.
تكرار الكود (Code duplication). قواعد التحقق ومنسّقات القيم ومنطق التواريخ غالبًا ما تحتاج إلى وجودها على جانبي حدود الشبكة. يدفع العرض في جانب العميل كل شيء إلى جانب العميل، لكنك ما زلت بحاجة إلى مطابقة عقد الخادم.
نمط جلب البيانات (Data fetching pattern). تنتهي كل تطبيقات العرض في جانب العميل تقريبًا عند النمط useEffect ← fetch ← setState، وهو ما يكوّن شلالات من الطلبات (request waterfalls) عندما تجلب المكوّنات البيانات بالتسلسل. تخفّف مكتبات مثل TanStack Query أو SWR من ذلك عبر التخزين المؤقت (memoization) وإزالة التكرار، لكن الشكل الجوهري — الجلب بعد التركيب — يبقى كما هو.
استخراج المزيد من العرض في جانب العميل
عندما يكون العرض في جانب العميل هو الخيار الصحيح، تعامل مع حزمة JavaScript بوصفها الميزانية التي تحدّد تجربة المستخدم.
- حدّد ميزانية واقعية. اجعل الهدف أقل من نحو 170 كيلوبايت من JavaScript مضغوطة على المسار الحرج عند التحميل الأول. قِس على جهاز من الفئة المتوسطة بعد تقييد سرعة الشبكة، لا على حاسوبك المحمول.
- قسّم الكود على مستوى المسار (Route-level code splitting). استخدم
React.lazyوimport()الديناميكي كي لا يرسل كل مسار إلا ما يحتاجه. وأداة لتحليل حجم الحزم مثلrollup-plugin-visualizerأوwebpack-bundle-analyzerستخبرك ما الذي يُضخّم حجم كل قطعة (chunk). - استخدم Suspense للتحميل التصريحي (declarative loading). غلّف حدود التحميل الكسول بـ `` حتى تكون حالات التحميل صريحة ويمكن التنبؤ بها، ولا تتأثر بقية الصفحة.
- حمّل السلسلة الحرجة مسبقًا (Preload the critical chain). استخدم
لقطع الدخول ولأول طلب بيانات. هذا يقلّل زمن الانتظار في مقدمة الطابور الذي يضر بمقياس LCP. - أجّل الواجهات غير الحرجة (Defer non-critical UI). الأدوات أسفل الطيّة، وفقاعات المحادثة، والتعليقات، وسكربتات الطرف الثالث — حمّلها عبر
IntersectionObserverأو بعد أن يصبح المحتوى الرئيسي تفاعليًا. - هيكل التطبيق + عامل الخدمة (App shell + service worker). خزّن الهيكل والأصول الثابتة (static assets). تصبح الزيارات المتكررة شبه فورية وتعمل دون اتصال.
- واجهة هيكلية بدل المؤشرات الدوارة (Skeleton UI, not spinners). الهيكل الذي يطابق التخطيط النهائي يمنع CLS ويبدو أسرع من مؤشر دوران في المنتصف.
- استخدم مكتبة لجلب البيانات (Use a data fetching library). تمنحك TanStack Query أو SWR أو Apollo Client إزالة تكرار الطلبات، والتخزين المؤقت، ودلالات «قديم مع إعادة تحقق» (stale-while-revalidate)، ونموذجًا موحّدًا للتحميل والأخطاء. وهي ليست اختيارية لتطبيقات العرض في جانب العميل الجادة.
كيف غيّرت React 18+ الصورة
تخفّف مجموعة من ميزات العرض المتزامن بعض نقاط الألم الكلاسيكية في العرض في جانب العميل:
- التجميع التلقائي (Automatic batching) يدمج عدة تحديثات للحالة (state) داخل استدعاءات غير متزامنة في عرض واحد، وهو ما كان يتطلب سابقًا
unstable_batchedUpdates. useTransitionيتيح لك وسم التحديثات المكلفة (تغييرات التصفية، وإعادة العرض (re-render) للقوائم الكبيرة) بأنها غير عاجلة، فتبقى الواجهة مستجيبة.useDeferredValueيُبقي حقل الإدخال سريع الاستجابة بينما تلحق به عرض مشتق (نتائج البحث، والإكمال التلقائي).- الترطيب الانتقائي (Selective hydration) ليس ميزة خاصة بالعرض في جانب العميل، لكن المجدول نفسه يشغّل العرض المتزامن في تطبيقات العرض في جانب العميل، مما يجعل التحديثات الكبيرة قابلة للمقاطعة.
هذه الميزات لا تغيّر الكلفة الجوهرية للعرض في جانب العميل — فشيفرة JavaScript لا بد أن تصل قبل أن يُعرض أي شيء — لكنها تنعّم تجربة التفاعل بعد أن يكون التطبيق قيد التشغيل.
حين تتجاوز حدود العرض في جانب العميل
إذا بدأ تطبيقك الذي يعتمد على العرض في جانب العميل كنموذج أوّلي سريع، وهو الآن يخدم مستخدمين حقيقيين، فإن مسار الترقية معروف جيدًا:
- انتقل إلى Next.js أو Remix أو TanStack Start للحصول على العرض من الخادم أو التوليد الثابت للاستجابة الأولية مع الإبقاء على نمط التنقل الخاص بتطبيقات الصفحة الواحدة الذي اعتدته.
- حوّل المسارات العامة المعتمدة على المحتوى إلى مكوّنات الخادم (Server Components) (صفر شيفرة JavaScript في جانب العميل للأجزاء الثابتة) وأبقِ الشاشات كثيرة التفاعل في جانب العميل باستخدام
'use client'. - استخدم البث من الخادم (streaming SSR) لي يرى المستخدم أول رسم ذي معنى قبل أن ينتهي تحميل أي بيانات.
تشرح بقية الأنماط في هذا القسم كلًّا من تلك البدائل واحدًا واحدًا.