أنماط تصميم تطبيقات الويب › أنماط React وNext.js 2 / 3 السابق التالي

العرض في جانب الخادم

العرض في جانب الخادم (Server-side rendering) يحوّل شجرة مكوّنات (component) React إلى HTML على الخادم (server) ويرسل ذلك HTML ضمن الاستجابة الأولية. يرسم المتصفح شيئًا ذا معنى قبل تحليل أي شيفرة JavaScript من التطبيق. وبعد وصول المستند، تعمل React مرة أخرى في جانب العميل (client) وتُطبّق الترطيب (hydration) على ترميز الصفحة — فترفق مستمعي الأحداث وتعيد ربط DOM بشجرة المكوّنات حتى يصبح تفاعليًا.

العرض في جانب الخادم هو الخيار الافتراضي المناسب عندما تعتمد محتويات الصفحة على الطلب — المستخدم المسجّل الدخول، أو كعكة (cookie)، أو مجموعة في اختبار A/B، أو ترويسة تحديد موقع جغرافي — و عندما تُقرأ الصفحة بكثرة تكفي لأن يستحق العرض لكل طلب تكلفته. صفحة تفاصيل منتج مخصّصة لمنطقة المشاهد، ولوحة تحكم تتطلب مصادقة، وصفحة نتائج بحث مُعامَلة بسلسلة الاستعلام (query string): هذه هي موطن العرض في جانب الخادم.

ما الذي يكسبك إياه العرض في جانب الخادم

الجانب النتيجة مع العرض في جانب الخادم
LCP في عمليات التحميل الباردة أفضل — المحتوى الرئيسي موجود في HTML الأولي.
تحسين محركات البحث ومعاينات الروابط ترى زواحف البحث ترميزًا مُعرضًا بالكامل، دون حاجة إلى تنفيذ JS.
التخصيص أصلي — ترويسات الطلبات والكعكات وبيانات الجلسة متاحة وقت العرض.
TTFB أبطأ من الثابت — العمل يحدث لكل طلب.
كلفة الخادم أعلى من الثابت — كل طلب ينفّذ عملية العرض.
كلفة الترطيب (hydration) حقيقية — لا يزال جانب العميل يحتاج إلى JS لجعل الواجهة تفاعلية.

يُفهم العرض في جانب الخادم على أفضل نحو بوصفه مقايضة ضد العرض الثابت (static rendering) (TTFB أسرع، ودون عمل لكل طلب) وضد العرض في جانب العميل (CSR) (كلفة ترطيب أقل بكثير، وأول رسم أصغر حجماً).

خط عرض في جانب الخادم حديث باستخدام renderToPipeableStream

استبدلت React 18 واجهات الخادم الأقدم المعتمدة على النصوص بمتغيّرات تعتمد على البث (streaming). على بيئات تشغيل Node تستخدم renderToPipeableStream؛ وعلى بيئات Web/Edge (Cloudflare Workers، Vercel Edge، Deno) تستخدم renderToReadableStream. كلاهما يدعم `` للبث الجزئي ويفتح ميزات React المتزامنة على الخادم.

الطرف المقابل في جانب العميل للعرض في جانب الخادم هو hydrateRoot (وليست createRoot، وليست ReactDOM.hydrate القديمة التي أُزيلت في React 19).

إليك خادم Express بسيطًا يعرض صفحة تفاصيل منتج:

server.jsx · client.jsx · ProductPage.jsx

import express from "express";
import { renderToPipeableStream } from "react-dom/server";
import ProductPage from "./ProductPage";

const app = express();
app.use("/static", express.static("dist"));

app.get("/products/:id", async (req, res) => {
  const product = await loadProduct(req.params.id);

let didError = false;
  const { pipe } = renderToPipeableStream(
    <ProductPage product={product} />,
    {
      bootstrapModules: ["/static/client.js"],
      onShellReady() {
        res.statusCode = didError ? 500 : 200;
        res.setHeader("Content-Type", "text/html");
        pipe(res);
      },
      onShellError(err) {
        res.statusCode = 500;
        res.setHeader("Content-Type", "text/html");
        res.send("<h1>Something went wrong</h1>");
      },
      onError(err) {
        didError = true;
        console.error(err);
      },
    }
  );
});

app.listen(3000);

هناك بعض التفاصيل التي تستحق إبرازها:

  • onShellReady يُطلق بمجرد أن يصبح الجزء الخارج لأي حدود من نوع `` قابلًا للعرض. فيُرسل الخادم ذلك الهيكل فورًا، فيبدأ المتصفح الرسم بينما تستمر البيانات الأبطأ في الوصول.
  • onAllReady هو الاستدعاء البديل — انتظره بدلًا من ذلك عندما تحتاج إلى HTML الكامل (زواحف البحث التي لا تنفّذ السكربتات المتدفقة، وخلاصات RSS، وعرض البريد الإلكتروني).
  • bootstrapModules تخبر React بأي السكربتات أن تحقنها، بحيث يبدأ الترطيب تلقائيًا عند وصول الهيكل.
  • يمكن لمكوّن Reviews أن يرمي Promise (عبر طبقة بيانات متوافقة مع Suspense). وعندما يُحلّ، تبثّ React قطعة HTML إضافية مع سكربت مضمّن يستبدل المحتوى الاحتياطي في موضعه. ولا حاجة إلى أي استطلاع (polling) في جانب العميل.

الترطيب (hydration)، ولماذا هو الجزء المكلف

الترطيب هو الخطوة التي تمشي فيها React فوق DOM المُعرض على الخادم، وتطابق كل عقدة مع شجرة المكوّنات، وترفق مستمعي الأحداث. وهو لا ينتج أي تغيير مرئي، لكنه يشغّل شجرة المكوّنات كاملةً مرة واحدة على الأقل — ولهذا فإن كلفة الترطيب تساوي تقريبًا كلفة أول عرض في جانب العميل.

هذا يخلق «وادي الغرابة» الخاص بالعرض في جانب الخادم: تبدو الصفحة جاهزة، لكن النقرات والمدخلات تُسقَط بصمت حتى يُحلَّل الحزمة ويكتمل الترطيب. والمقياس الذي تتابعه Google الآن لهذا الغرض هو التفاعل حتى الرسم التالي (Interaction to Next Paint, INP)، الذي حلّ محل FID في مؤشرات Core Web Vitals في مارس 2024.

ثلاثة أنماط تخفّض كلفة الترطيب:

  • البث من الخادم (streaming SSR) مع `` يتيح لأجزاء من الصفحة أن تُرطَّب عند وصول شيفرتها، بدلًا من الانتظار حتى الحزمة كاملة.
  • الترطيب الانتقائي (Selective hydration) (المدمج في React 18+) يعطي الأولوية لترطيب المكوّن الذي يتفاعل معه المستخدم حاليًا.
  • مكوّنات خادم React (React Server Components) تلغي الترطيب لأي مكوّن لا يحتاج إلى أن يكون تفاعليًا — فشيفرته لا تُرسَل أصلًا.

العرض في جانب الخادم مع Next.js App Router

في App Router، يكون العرض من الخادم هو الوضع الافتراضي ولا وجود لـ getServerSideProps. تصبح الصفحة ديناميكية — أي معروضة لكل طلب — فور قراءتها لبيانات وقت الطلب. وقراءة cookies() أو headers() أو searchParams تُدخل المسار تلقائيًا في العرض الديناميكي. (وفي Next.js 15 أصبحت هذه الواجهات غير متزامنة ويجب انتظارها.)

// app/dashboard/page.tsx

import { cookies } from "next/headers";\n

export default async function Dashboard() {

const cookieStore = await cookies();

const session = cookieStore.get("session");\n

const user = await fetchUser(session?.value);

const widgets = await fetchWidgets(user.id);\n

return (

<section>

<h1>Welcome back, {user.name}</h1>

<WidgetGrid widgets={widgets} />

</section>

);

}`

وهناك أمور تغيّرت في Next.js 15 تؤثر على العرض في جانب الخادم تحديدًا:

  • لم تعد استدعاءات fetch() مخزّنة مؤقتًا (cached) افتراضيًا. أنت تفعّل ذلك لكل طلب عبر fetch(url, { cache: "force-cache" }) أو خيار next.revalidate. وهذا يجعل تمييز العرض من الخادم عن العرض الثابت أسهل في التفكير.
  • أصبحت cookies() وheaders() وdraftMode()، وكذلك params/searchParams الديناميكية، غير متزامنة ويجب انتظارها.
  • يتيح لك التوجيه الجديد 'use cache' (تجريبي، خلف علم) وسم دالة أو مكوّن بأنه قابل للتخزين المؤقت (cacheable) بمعزل عن عمليات الجلب.

العرض في جانب الخادم على الحافة (Edge SSR)

تشغيل العرض في جانب الخادم على بيئة تشغيل حافة (Cloudflare Workers، Vercel Edge Functions، Deno Deploy) ينقل عملية العرض فعليًا إلى قرب المستخدم. ينخفض TTFB انخفاضًا حادًا — عادةً من 200–600 مللي ثانية على مصدر مركزي إلى 30–80 مللي ثانية عند الحافة — ويقترب زمن البدء البارد (cold start) في الحوسبة بلا خوادم من الصفر على المنصات الحديثة.

والمقايضات التي يجب وزنها:

  • بيئة الحافة هي مجموعة جزئية من Node — لا وحدات أصلية (native modules)، ولا نظام ملفات، ودعم محدود لـ Buffer. استخدم renderToReadableStream لا renderToPipeableStream.
  • لدوال الحافة حدود ضيقة للمعالج والذاكرة (عادةً 50–128 ميغابايت وعشرات الأجزاء من الثانية من زمن المعالج لكل طلب).
  • لا تزال عمليات جلب البيانات تحتاج إلى الوصول إلى مصدر. إذا كانت قاعدة بياناتك في us-east-1 وكان عامل الحافة لديك يعمل في ساو باولو، فقد أضفت للتو زمن تأخير. اقترن العرض على الحافة ببيانات مُكرَّرة على الحافة (Cloudflare D1، Turso، Upstash، نسخ القراءة في Neon).

متى لا تستخدم العرض في جانب الخادم

  • المحتوى الثابت تمامًا (الصفحات التسويقية، ومنشورات المدونة، والتوثيق): استخدم العرض الثابت. HTML نفسه، ودون كلفة لكل طلب.
  • التطبيقات عالية التفاعل ذات هيكل مستقر (محرّر بأسلوب Figma): كلفة الترطيب تفوق فائدة العرض في جانب الخادم. العرض في جانب العميل مع هيكل تطبيق يكفي.
  • الصفحات التي تتحمّل بضع دقائق من التخلّف (staleness): استخدم إعادة التوليد التدريجي (ISR) مع إعادة التحقق (revalidation). تحتفظ بالتخزين المؤقت (caching) عند حافة شبكة CDN مع الحصول على محتوى طازج.

قائمة تحقق عملية

قبل شحن العرض في جانب الخادم إلى الإنتاج، تحقّق من الآتي:

  • أنك تستخدم renderToPipeableStream (في Node) أو renderToReadableStream (عند الحافة)، لا renderToString المُهملة للعرض الأساسي.
  • أن جانب العميل يستخدم hydrateRoot لا ReactDOM.hydrate.
  • من أن هناك معالج onError يسجّل أخطاء البث، وأن هناك حدًا واحدًا على الأقل يلتقط فشل العرض.
  • من أن `` يغلّف البيانات البطيئة كي يتمكّن الهيكل من البث أولًا.
  • من أن عمليات جلب البيانات طويلة المدة لديك تملك ذاكرة مؤقتة (caching) على نطاق الطلب (لكل عملية عرض) لتفادي الاستدعاءات المكرّرة.
  • من أن حزمتك مقسّمة على مستوى المسار — فترطيب الصفحة الحالية لا يحتاج إلا إلى شيفرة JS الخاصة بها.

الأنماط التالية التي سننظر إليها — البث من الخادم، والعرض الثابت، وإعادة التوليد التدريجي (ISR)، والترطيب التدرّجي (Progressive Hydration)، ومكوّنات خادم React — تُحسّن كل منها جزءًا من هذه القاعدة.