> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# CSR و SSR و SSG: أين ترسم صفحة الويب ومتى؟
- URL: https://arabroot.io/articles/الفرق-بين-csr-و-ssr-و-ssg-ومتى-تختار-كلا-منها/
- Published: 2026-10-30T18:16:06.000Z
- Updated: 2026-10-30T20:29:27.000Z
- Description: قسنا نفس صفحة المتجر مرسومة في المتصفح وفي السيرفر ومولدة مسبقاً على شبكة بطيئة في Chrome، لترى متى يظهر المحتوى ومتى تعمل الأزرار وماذا يرى محرك البحث.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, تطوير الواجهات والجوال Frontend & Mobile

لنفرض أن فريق التسويق في متجر القهوة والتمور نشر رابط صفحة «تشكيلة الموسم» في حساب المتجر، وبعد ساعة جاءك أحدهم بلقطة شاشة: معاينة الرابط تظهر عنوان الموقع فقط، بدون اسم الصفحة ولا وصفها. وفي نفس الأسبوع تلاحظ في أدوات مشرفي المواقع أن صفحات التصنيفات تأخذ أياماً حتى تظهر في نتائج البحث، ويشتكي عميل يستخدم جوالاً قديماً على شبكة ضعيفة من أن الصفحة تبقى بيضاء ثانيتين قبل أن يظهر أي شيء. وعندما تفتح الصفحة على جهازك المكتبي تظهر في لمح البصر، ولا تجد أي خطأ.

والسبب في الحالات الثلاث هو المكان الذي ترسم فيه الصفحة، فواجهة المتجر مبنية بإطار JavaScript يرسم كل شيء في المتصفح، والسيرفر يرسل صفحة HTML شبه فارغة فيها وسم `<div>` واحد وملف JavaScript. فبرنامج المعاينة لا يشغل JavaScript فلا يرى شيئاً، ومحرك البحث يحتاج أن يضع الصفحة في طابور الرسم قبل أن يعرف محتواها، والجوال البطيء يحتاج أن ينزل الكود ويشغله ثم يطلب البيانات قبل أن يرسم سطراً واحداً. وهذا الاختيار له اسم، وهو الرسم في المتصفح CSR، وله بديلان مشهوران: الرسم في السيرفر SSR، والتوليد المسبق SSG. وهذا المقال من مرحلة [هندسة الواجهات وتطبيقات الويب Frontend](https://arabroot.io/start/#stage-web-frontend)، وقد رأينا في [مقال عمل المتصفح من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84-dom-%D9%88%D8%A7%D9%84%D8%AA%D8%AE%D8%B7%D9%8A%D8%B7-%D9%88%D8%A7%D9%84%D8%B1%D8%B3%D9%85/) كيف يتحول HTML إلى بكسلات، وهنا نسأل سؤالاً قبله: من الذي يكتب هذا الـ HTML، ومتى؟

وسوف نناقش في هذا المقال ما يلي:

- كيف وصلنا إلى ثلاث طرق: من صفحات السيرفر إلى التطبيقات أحادية الصفحة SPA ثم العودة إلى السيرفر.
- كيف تعمل كل طريقة من الداخل، والإحياء Hydration الذي يربط الـ HTML الجاهز بالـ JavaScript.
- المقاييس التي تحكم بها على السرعة: الزمن حتى أول بايت TTFB، وأول رسم للمحتوى FCP، وأكبر رسم للمحتوى LCP، ومتى تصبح الصفحة تفاعلية.
- التجربة العملية: نفس الصفحة بالطرق الثلاث، مقيسة في Chrome بدون واجهة على شبكة بطيئة، مع ما يراه برنامج لا يشغل JavaScript.
- محركات البحث ومعاينة الروابط، والتكلفة على السيرفر، والحلول الهجينة مثل ISR والجزر Islands.
- الأخطاء الشائعة، وكيف تختار لكل صفحة، وقائمة تحقق.

## كيف وصلنا إلى ثلاث طرق للرسم Rendering؟

في بداية الويب كان كل شيء يرسم في السيرفر، فعندما تطلب صفحة يقرأ كود PHP أو Java أو غيرهما البيانات من قاعدة البيانات، ويكتب HTML كاملاً، ويرسله، والمتصفح يعرضه. وكل نقرة على رابط أو إرسال نموذج يعني طلباً جديداً وصفحة جديدة كاملة. وهذا النموذج بسيط وسريع في أول عرض، ولكنه ثقيل في التفاعل، فإضافة منتج إلى السلة تعيد تحميل الصفحة كلها.

ثم جاءت مرحلة التطبيقات أحادية الصفحة Single-Page Applications، حيث يرسل السيرفر صفحة فارغة تقريباً مع ملف JavaScript كبير، وهذا الملف يرسم كل شيء ويطلب البيانات من API بصيغة JSON، وعند التنقل بين الصفحات لا يعاد تحميل أي شيء. وهذا هو الرسم في المتصفح Client-Side Rendering، واختصاراً CSR، وهو ما تحصل عليه افتراضياً عندما تنشئ مشروعاً جديداً بـ React أو Vue أو Svelte كما في مقال أطر الواجهات من نفس المرحلة. والتفاعل هنا ممتاز، ولكن الثمن ظهر في أول تحميل وفي محركات البحث، وهو بالضبط ما رأيته في قصة المتجر.

لذلك عادت الصناعة إلى السيرفر، ولكن بنفس المكونات هذه المرة: السيرفر يشغل نفس كود الواجهة، ويرسم HTML كاملاً للطلب، ثم يرسل معه الـ JavaScript ليتولى التفاعل بعد التحميل. وهذا هو الرسم في السيرفر Server-Side Rendering، واختصاراً SSR. وفي نفس الوقت انتشرت أدوات التوليد المسبق Static Site Generation، واختصاراً SSG، التي ترسم الصفحات مرة واحدة وقت البناء وتحفظها كملفات، فيرسلها السيرفر أو شبكة توصيل المحتوى CDN كما هي. ويجمع فريق Chrome هذه الطرق ومقارنتها في مقال [Rendering on the Web](https://web.dev/articles/rendering-on-the-web?ref=arabroot.io)، وهو أفضل نقطة بداية إذا أردت قراءة أوسع.

## كيف تعمل CSR و SSR و SSG من الداخل؟

الصورة التالية تبين ما يحدث في الطرق الثلاث لنفس الصفحة، من أول طلب إلى أن يصبح الزر قابلاً للنقر:

![ثلاثة صفوف: CSR حيث يرسل السيرفر HTML شبه فارغ من 979 بايت ثم ينزل المتصفح JavaScript ثم يطلب البيانات من api ثم يبني الصفحة، وSSR حيث ينتظر السيرفر قاعدة البيانات ويرسم HTML كاملاً ثم يعرضه المتصفح فوراً ثم ينزل JavaScript ثم يحدث الإحياء، وSSG حيث ترسم الصفحة مرة وقت البناء إلى ملف ثم يرسله السيرفر أو CDN كما هو ثم يعرضه المتصفح ثم يحدث الإحياء](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-01-three-ways.webp)

المربع المحاط باللون هو اللحظة التي يرى فيها المستخدم المنتجات

### الرسم في المتصفح CSR

في CSR يصل إلى المتصفح ملف HTML فيه عنصر فارغ مثل `<div id="root"></div>` ووسم `<script>`. والمتصفح لا يستطيع أن يرسم شيئاً مفيداً بعد، فينزل الـ JavaScript، ويحللها ويشغلها، والكود يطلب البيانات من الـ API، وينتظر الرد، ثم يبني الـ DOM ويرسم. أي أن أمام المستخدم سلسلة من ثلاث رحلات على الشبكة على الأقل، وبينها تشغيل كود على معالج الجهاز، والشاشة فارغة طوال هذا الوقت. وفي المقابل، عندما يرسم الكود الصفحة تكون تفاعلية فوراً، لأن نفس الكود الذي رسمها هو الذي ربط الأحداث، والتنقل بعد ذلك بين الصفحات سريع جداً لأن الكود موجود ويطلب البيانات فقط.

وقد يتساءل البعض: هل يمكن تحسين CSR دون الانتقال إلى السيرفر؟ والإجابة نعم إلى حد ما، فالمشكلة الأساسية هي السلسلة المتتابعة: HTML ثم JavaScript ثم API. والطريقة الأولى أن تكسر هذه السلسلة، فتضع في الـ HTML وسم `<link rel="preload">` للـ JavaScript، وتبدأ طلب البيانات في سكربت صغير داخل الصفحة قبل أن ينتهي تنزيل الحزمة الكبيرة، فيتوازى الطلبان بدلاً من أن يتتابعا. والطريقة الثانية تقسيم الكود Code Splitting، حتى تنزل الصفحة الأولى الكود الذي تحتاجه فقط. والطريقة الثالثة أن تضع في الغلاف هيكلاً رمادياً Skeleton بشكل الصفحة، فيرى المستخدم شيئاً بدلاً من البياض. ولكن لاحظ أن هذه كلها تقلل الانتظار ولا تلغيه، وأن الهيكل الرمادي لا يحمل أي محتوى، فلن يفيد الزاحف ولا معاينة الرابط بشيء.

### الرسم في السيرفر SSR

في SSR يستقبل السيرفر الطلب، ويجلب البيانات من قاعدة البيانات، ثم يشغل نفس مكونات الواجهة ويحولها إلى نص HTML بدالة مثل [renderToString](https://react.dev/reference/react-dom/server/renderToString?ref=arabroot.io) في React، ويرسل الصفحة كاملة. فيرسم المتصفح المنتجات بمجرد وصول الـ HTML، دون أن ينتظر أي JavaScript. ولاحظ أن السيرفر لا يرسل شيئاً حتى تنتهي قاعدة البيانات، أي أن بطء قاعدة البيانات يظهر مباشرة في الزمن حتى أول بايت، وسوف نرى هذا بالأرقام. والحل الحديث لهذا هو الإرسال المتدفق Streaming بدالة مثل [renderToPipeableStream](https://react.dev/reference/react-dom/server/renderToPipeableStream?ref=arabroot.io)، حيث يرسل السيرفر رأس الصفحة وهيكلها فوراً، ثم يرسل الأجزاء التي تنتظر البيانات عندما تجهز.

### التوليد المسبق SSG

في SSG يحدث نفس الرسم الذي في SSR، ولكن مرة واحدة وقت البناء Build، وليس مع كل طلب. فأداة البناء تجلب البيانات، وترسم كل صفحة إلى ملف HTML، وتضع الملفات في مجلد يرفع إلى السيرفر أو إلى CDN. وعندما يطلب المستخدم الصفحة يرسل له الملف كما هو، دون قاعدة بيانات ودون تشغيل أي كود. ولذلك فهي أسرع الطرق وأرخصها، ولكن المشكلة واضحة: الصفحة تعرض البيانات كما كانت لحظة البناء، فإذا تغير سعر منتج أو نفد من المخزون فلن يظهر ذلك حتى تبني الموقع من جديد، وإذا كان عندك مئة ألف منتج فالبناء نفسه قد يأخذ وقتاً طويلاً.

وفي تجربتنا رسم `build.mjs` صفحة SSG في 68.3 ms بحسب الملف `build-output.txt`، وهذا رقم صغير لصفحة واحدة، ولكن الحساب يتغير مع الحجم: متجر فيه عشرون ألف منتج يعني عشرين ألف صفحة في كل بناء، وكل تعديل على سعر واحد يحتاج بناء الصفحة التي فيها هذا المنتج على الأقل. لذلك تدعم أدوات التوليد المسبق البناء التدريجي، أي إعادة بناء الصفحات التي تغيرت بياناتها فقط، أو توليد الصفحة عند أول طلب لها ثم حفظها، وهذه الفكرة هي التي توصلنا إلى ISR في قسم التكلفة.

### الإحياء Hydration: الخطوة التي ينساها الجميع

في SSR وSSG يصل الـ HTML جاهزاً، والأزرار مرسومة على الشاشة، ولكن لا يوجد على أي زر معالج نقرة، لأن الـ HTML نص فقط. ولكي تعمل الصفحة يجب أن ينزل نفس كود الواجهة إلى المتصفح، ويشغل المكونات من جديد بنفس البيانات، ويطابق النتيجة مع الـ HTML الموجود عنصراً بعنصر، ثم يربط الأحداث. وهذه العملية تسمى الإحياء Hydration، وفي React تتم بالدالة [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot?ref=arabroot.io).

ولهذا شرطان مهمان. الأول أن يكون في المتصفح نفس البيانات التي رسم بها السيرفر، لذلك يكتب السيرفر البيانات في الصفحة نفسها كـ JSON داخل وسم `<script>`، وهذا يعني أن البيانات ترسل مرتين: مرة كـ HTML ومرة كـ JSON. والثاني أن يرسم المتصفح نفس ما رسمه السيرفر حرفياً، فإذا كان في المكون تاريخ اليوم أو رقم عشوائي أو شيء يعتمد على `window`، فسوف يختلف الناتج، ويظهر خطأ عدم التطابق Hydration Mismatch. والأهم من الشرطين أن هناك فترة بين ظهور الصفحة وانتهاء الإحياء تبدو فيها الصفحة جاهزة وهي ليست كذلك، وهذه الفترة هي ما سوف تقيسه التجربة في قسم «ماذا يحدث إذا».

## المقاييس: TTFB و FCP و LCP والتفاعل

قبل التجربة نحتاج أن نتفق على ما نقيسه، والمقاييس الأربعة التالية هي التي تفرق بين الطرق الثلاث:

- **الزمن حتى أول بايت TTFB:** من بداية الطلب إلى وصول أول بايت من الرد. وهو يقيس السيرفر والشبكة، ويقترح فريق Chrome في [صفحة TTFB](https://web.dev/articles/ttfb?ref=arabroot.io) أن يكون 0.8 ثانية أو أقل كتقريب عام.
- **أول رسم للمحتوى FCP:** أول لحظة يرسم فيها المتصفح نصاً أو صورة من الصفحة، كما في [صفحة FCP](https://web.dev/articles/fcp?ref=arabroot.io). وفي CSR تكون الشاشة قبلها بيضاء.
- **أكبر رسم للمحتوى LCP:** اللحظة التي يرسم فيها أكبر عنصر ظاهر، وهو من مقاييس [Core Web Vitals](https://web.dev/articles/vitals?ref=arabroot.io)، والقيمة الجيدة بحسب [صفحة LCP](https://web.dev/articles/lcp?ref=arabroot.io) هي 2.5 ثانية أو أقل في الشريحة 75% من الزيارات.
- **متى تصبح الصفحة تفاعلية:** لا يوجد له مقياس قياسي واحد، وفي التجربة نعرفه بأنه اللحظة التي انتهى فيها الرسم أو الإحياء ورسم المتصفح الإطار التالي، أي أن النقرة بعدها تعمل. وما يحدث للنقرة نفسها يقيسه مقياس INP الذي شرحناه في [مقال حلقة الأحداث](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/).

## التجربة العملية Lab: نفس الصفحة بثلاث طرق

الآن نقيس. الصفحة صفحة تصنيف في المتجر: رأس فيه عداد السلة، وعنوان وفقرة تعريفية، ثم 200 منتج، لكل منتج اسم ووصف وسعر وزر «أضف إلى السلة». ومكون الصفحة مكتوب مرة واحدة بـ React، ونفس الكود يعمل في السيرفر وفي المتصفح، ويقدم سيرفر واحد مكتوب بـ Node.js الصفحة بالطرق الثلاث على ثلاثة مسارات.

### المكون Component والسيرفر

المكون مكتوب بـ `createElement` بدلاً من JSX، حتى يستطيع Node.js أن يستورده مباشرة دون خطوة بناء في السيرفر:

```javascript
// The same page for the three strategies: a category page of an online store.
// Plain React.createElement (no JSX) so Node.js can import it directly on the server.
import { createElement as h, useState } from "react";

export function ProductPage({ data }) {
  const [cart, setCart] = useState(0);
  return h("main", null,
    h("header", { className: "bar" }, h("b", null, "متجر القهوة والتمور"), h("span", { id: "cart" }, `السلة: ${cart}`)),
    h("section", { className: "hero" },
      h("h1", null, data.category.title),
      h("p", null, data.category.intro)),
    h("ul", { className: "grid" }, data.products.map((p) =>
      h("li", { key: p.id },
        h("h2", null, p.name),
        h("p", null, p.description),
        h("span", { className: "price" }, `${p.price} ر.س`),
        h("button", { onClick: () => setCart((c) => c + 1) }, "أضف إلى السلة")))));
}
```

وفي نفس الملف دالة `loadCategory` تمثل قاعدة البيانات، وترجع العنوان والفقرة و200 منتج. أما السيرفر فهو كما يلي:

```javascript
const DB_MS = Number(process.env.DB_MS ?? 200);
const PORT = Number(process.env.PORT ?? 19390);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const db = async () => { await sleep(DB_MS); return loadCategory(); };

createServer(async (req, res) => {
  const path = new URL(req.url, "http://x").pathname;
  if (path === "/csr") return send(req, res, "text/html; charset=utf-8", shell({ title: "متجر القهوة والتمور", script: "/assets/csr.js" }));
  if (path === "/ssr") {
    const data = await db();
    const t0 = performance.now();
    const html = renderPage(data, "/assets/hydrate.js");
    return send(req, res, "text/html; charset=utf-8", html, { "server-timing": `db;dur=${DB_MS}, render;dur=${(performance.now() - t0).toFixed(1)}` });
  }
  if (path === "/ssg") return send(req, res, "text/html; charset=utf-8", readFileSync("dist/ssg.html"));
  if (path === "/api/category") return send(req, res, "application/json", JSON.stringify(await db()));
  const file = `dist${path}`;
  if (path.startsWith("/assets/") && existsSync(file)) return send(req, res, "text/javascript", readFileSync(file));
  res.writeHead(404); res.end();
}).listen(PORT, "127.0.0.1", () => console.log(`listening on ${PORT}, DB_MS=${DB_MS}`));
```

وفي الكود أعلاه لاحظ التالي:

- `/csr` يرسل الغلاف Shell فقط، أي HTML بدون محتوى مع السكربت `csr.js`، والسكربت يطلب `/api/category` ثم يرسم بـ `createRoot`.
- `/ssr` ينتظر قاعدة البيانات الوهمية، والقيمة الافتراضية 200 ms، ثم يرسم بـ `renderToString` مع كل طلب، ويضيف ترويسة [Server-Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing?ref=arabroot.io) تقول كم أخذت قاعدة البيانات وكم أخذ الرسم، وهذه الترويسة تظهر في DevTools في لوحة Network.
- `/ssg` يقرأ ملفاً رسمه السكربت `build.mjs` مرة واحدة وقت البناء، ولا يلمس قاعدة البيانات أبداً.
- الدالة `send` تضغط كل رد بـ gzip إذا طلبه المتصفح، كما يفعل أي سيرفر حقيقي أو Reverse Proxy.

والدالة `renderPage` المشتركة بين SSR وSSG ترسم المكون، وتكتب البيانات داخل الصفحة كـ JSON، بعد استبدال `<` بـ `\u003c` حتى لا يستطيع نص في البيانات أن يغلق وسم السكربت ويحقن كوداً:

```javascript
export function shell({ title, body = "", data = null, script }) {
  const json = data ? `<script id="data" type="application/json">${JSON.stringify(data).replace(/</g, "\\u003c")}</script>` : "";
  return `<!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>
<meta name="description" content="قهوة عربية وتمور وهيل وزعفران، توصيل خلال يومين.">
<style>${CSS}</style></head><body><div id="root">${body}</div>${json}<script type="module" src="${script}"></script></body></html>`;
}

export function renderPage(data, script) {
  return shell({ title: data.category.title, body: renderToString(h(ProductPage, { data })), data, script });
}
```

وفي المتصفح يقرأ `client-hydrate.js` هذا الـ JSON ويستدعي `hydrateRoot`، ثم يسجل لحظة انتهاء الإحياء بعد الإطار التالي في `window.__ready`. والتشغيل كما يلي:

```bash
npm install
node build.mjs
NODE_ENV=production node server.mjs
node measure.mjs http://127.0.0.1:19390 7
```

### كيف قسنا؟ التبطيء Throttling في Chrome

السكربت `measure.mjs` يقوم بثلاثة أمور. أولاً يطلب الصفحات الثلاث من Node.js مباشرة، مثل أي برنامج لا يشغل JavaScript، ويعد ما فيها. ثانياً يقيس الزمن حتى أول بايت بدون أي تبطيء. وثالثاً يقود Chrome 156 بدون واجهة عبر المساعد `scripts/lab/browser.mjs`، ويضبط الشبكة بالأمر [Network.emulateNetworkConditions](https://chromedevtools.github.io/devtools-protocol/tot/Network/?ref=arabroot.io#method-emulateNetworkConditions) على تأخير 150 ms وسرعة 1.6 Mbps، وهي قريبة من إعداد Slow 4G في [إعدادات التبطيء في DevTools](https://developer.chrome.com/docs/devtools/settings/throttling?ref=arabroot.io)، ويبطئ المعالج أربع مرات، ويعطل الذاكرة المؤقتة Cache، ثم يحمل كل صفحة ثماني مرات بالتناوب ويحذف الجولة الأولى ويأخذ الوسيط للسبع الباقية.

### الخطوة الأولى: ما الذي يراه برنامج لا يشغل JavaScript؟

هذا أول جزء من المخرج:

```text
== raw HTML, no JavaScript (what a simple crawler or link preview gets)
csr: 979 bytes of HTML, <h1>: (none), product names in HTML: 0
ssr: 88054 bytes of HTML, <h1>: القهوة والتمور: تشكيلة الموسم, product names in HTML: 200
ssg: 88054 bytes of HTML, <h1>: القهوة والتمور: تشكيلة الموسم, product names in HTML: 200
```

صفحة CSR فيها 979 بايت فقط، ولا يوجد فيها عنوان ولا اسم منتج واحد، وعنوان الصفحة في `<title>` هو العنوان العام للمتجر وليس عنوان التصنيف، لأن الغلاف واحد لكل الصفحات. أما SSR وSSG فكل منهما 88,054 بايت، فيها العنوان والمنتجات المئتان. ولاحظ أن الصفحتين متطابقتان بالبايت، لأنهما نفس الرسم من نفس البيانات، والفرق الوحيد هو متى حدث. والصورة التالية لقطات حقيقية من Chrome بعرض جوال: الأولى صفحة CSR مع تعطيل JavaScript بالأمر `Emulation.setScriptExecutionDisabled`، والثانية صفحة SSR بنفس الإعداد، والثالثة صفحة CSR مع JavaScript للمقارنة:

![ثلاث لقطات بعرض جوال: من اليسار صفحة CSR بدون JavaScript فارغة تماماً، ثم صفحة SSR بدون JavaScript فيها رأس المتجر والعنوان القهوة والتمور تشكيلة الموسم والفقرة وبطاقات المنتجات، ثم صفحة CSR مع JavaScript بنفس الشكل](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-06-crawler-view.webp)

من اليسار: CSR بدون JavaScript، ثم SSR بدون JavaScript، ثم CSR مع JavaScript

وهذا بالضبط ما رآه برنامج معاينة الروابط في قصة المتجر، فأغلب هذه البرامج تقرأ الـ HTML ووسوم [Open Graph](https://ogp.me/?ref=arabroot.io) مثل `og:title` و`og:image` من الرد الأول ولا تشغل أي JavaScript، فإذا كانت هذه الوسوم تضاف بالـ JavaScript فلن تراها أبداً.

### الخطوة الثانية: الزمن حتى أول بايت TTFB بدون تبطيء

```text
== time to first byte, no throttling, median of 7 requests
csr: 2 ms
ssr: 217 ms
ssg: 5 ms
```

CSR وSSG أرسلا أول بايت في 2 و5 ms، لأن كلاً منهما يرسل محتوى جاهزاً: الغلاف الثابت أو الملف المبني. أما SSR فأخذ 217 ms، منها 200 ms لقاعدة البيانات الوهمية، والباقي للرسم والضغط. أي أن SSR ينقل انتظار قاعدة البيانات من المتصفح إلى السيرفر، ولا يلغيه.

### الخطوة الثالثة: التحميل على شبكة بطيئة Throttling

```text
== 7 throttled loads each (150 ms RTT, 1.6 Mbps, CPU 4x), medians in ms
csr: HTML received 178, FCP 1392, LCP 1392 (<p>), interactive 1483, API done 1149 | HTML 915 B over the wire (979 B raw), JS 68697 B, items 200, hydration errors 0
ssr: HTML received 239, FCP 480, LCP 480 (<p>), interactive 1160 | HTML 5203 B over the wire (88054 B raw), JS 68757 B, items 200, hydration errors 0
ssg: HTML received 189, FCP 440, LCP 440 (<p>), interactive 1368 | HTML 5203 B over the wire (88054 B raw), JS 68757 B, items 200, hydration errors 0
```

وفي المخرج أعلاه لاحظ التالي:

- **أول رسم في CSR جاء في 1392 ms**، وقبله شاشة بيضاء، وانتهى طلب الـ API في 1149 ms، أي أن المتصفح نزل الـ HTML ثم الـ JavaScript ثم شغلها ثم طلب البيانات وانتظرها، وكل خطوة لا تبدأ إلا بعد التي قبلها.
- **أول رسم في SSR جاء في 480 ms، وفي SSG في 440 ms**، أي قرابة ثلث زمن CSR، لأن المنتجات موجودة في الـ HTML نفسه. والفرق بين SSR وSSG هنا صغير لأن قاعدة البيانات الوهمية سريعة، وسوف نغير ذلك بعد قليل.
- **LCP كان نفس FCP في كل الحالات**، وأكبر عنصر كان الفقرة التعريفية `<p>`، وكلها ضمن حد 2.5 ثانية، وهذه صفحة صغيرة بدون صور، ولو كان أكبر عنصر صورة منتج لأضيف زمن تنزيلها.
- **حجم الـ HTML المضغوط 5,203 بايت لـ SSR وSSG مقابل 915 لـ CSR**، أي أن المحتوى الكامل لمئتي منتج زاد الـ HTML 4.3 KB فقط بعد gzip، لأن النص المتكرر يضغط جيداً. أما الـ JavaScript فهي نفسها تقريباً في الثلاث، 68.7 KB بعد الضغط، لأن SSR وSSG يحتاجان نفس الكود للإحياء.
- **الصفحة صارت تفاعلية في 1160 و1368 ms في SSR وSSG**، وهذا قريب من CSR، لأن الثلاث تحتاج نفس الـ JavaScript. أي أن SSR وSSG حسنا وقت الظهور كثيراً، ولكنهما لم يحسنا وقت التفاعل بنفس القدر، وبينهما فترة تقارب 700 إلى 900 ms تكون فيها الصفحة ظاهرة والأزرار لا تعمل.
- **لا توجد أخطاء إحياء** في كل التحميلات، لأن المكون لا يعتمد على الوقت ولا على شيء يختلف بين السيرفر والمتصفح.

![خط زمني لكل طريقة من 0 إلى 1600 ms: CSR يصل فيها HTML ثم ينتظر JavaScript والـ api ثم يبني فيظهر FCP عند 1392 وتصبح تفاعلية عند 1483، وSSR يصل فيها HTML ثم FCP عند 480 ثم فترة برتقالية الصفحة ظاهرة والأزرار لا تعمل حتى 1160، وSSG يظهر FCP عند 440 ثم فترة برتقالية حتى 1368](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-02-timeline.webp)

SSR وSSG يظهران المحتوى أسرع بثلاث مرات، ولكن التفاعل يحتاج نفس الـ JavaScript

### كم تكلف البيانات المكررة للإحياء Hydration؟

ذكرنا أن الإحياء يحتاج نفس البيانات في المتصفح، ولذلك يكتبها السيرفر داخل الصفحة كـ JSON بجانب الـ HTML المرسوم منها. والسكربت الصغير `html-parts.mjs` يفصل الجزأين في صفحة SSG ويقيس كلاً منهما قبل الضغط وبعده:

```text
whole page             raw  88054  gzip  4903
page without the JSON  raw  49827  gzip  2632
JSON copy of the data  raw  38176  gzip  2307
```

نسخة البيانات وحدها 38,176 بايت من 88,054، أي 43% من الصفحة قبل الضغط، وبعد الضغط تقارب نصف ما يصل على الشبكة: 2,307 من 4,903 بايت. أي أن كل منتج يرسل مرتين، مرة كنص يراه المستخدم ومرة كبيانات يحتاجها الإحياء. وفي صفحة صغيرة مثل هذه الفرق بضعة كيلوبايتات، ولكن في صفحة تحمل وصفاً طويلاً لكل منتج أو مراجعات العملاء قد تصبح نسخة البيانات أكبر من الصفحة نفسها، وهي أيضاً نص يجب على المتصفح أن يحلله بـ `JSON.parse` على الخيط الرئيسي قبل الإحياء. والحل أن ترسل إلى المتصفح فقط البيانات التي تحتاجها الأجزاء التفاعلية، فالوصف الطويل الذي لا يتغير لا يحتاج إحياء، وهذه بالضبط الفكرة التي تبنى عليها الجزر ومكونات السيرفر React Server Components، حيث يرسم الجزء الثابت في السيرفر فقط ولا يرسل كوده ولا بياناته إلى المتصفح.

### ثلاث مشكلات واجهناها أثناء القياس Measurement

المشكلة الأولى كانت الأكبر، ففي أول تشغيل كان الزمن حتى أول بايت في SSR هو 334 ms مع قاعدة بيانات تأخذ 200 ms، وأظهرت ترويسة `Server-Timing` أن الرسم وحده يأخذ بين 275 و496 ms لصفحة من 200 منتج. والسبب أن السيرفر كان يعمل بدون `NODE_ENV=production`، فاستخدم React نسخة التطوير Development Build في السيرفر، وهذه النسخة تضيف فحوصاً وتحذيرات كثيرة تجعلها أبطأ بمراحل. وعندما أعدنا تشغيل السيرفر بـ `NODE_ENV=production` صار الرسم بين 2 و22 ms، ونزل الزمن حتى أول بايت إلى 217 ms. ولاحظ أن ملفات المتصفح لم تتأثر، لأن Vite يبني نسخة الإنتاج افتراضياً، فالمشكلة كانت في السيرفر وحده، وهذا خطأ سهل جداً أن يصل إلى الإنتاج في Container لا يضبط هذا المتغير. والمخرج الكامل لذلك التشغيل موجود في `run-output-devmode.txt` مع المقال.

والمشكلة الثانية كانت في قياس الزمن حتى أول بايت داخل Chrome، فقد ظهرت قيمة `responseStart` في التحميل المبطأ بضعة ملي ثوان، مع أن التأخير المضبوط 150 ms. فاختبرنا بتأخير 500 ms، ووجدنا أن Chrome يضيف التأخير المحاكى عند نهاية الرد وليس عند بدايته، فقيمة `responseStart` بقيت قريبة من الصفر، و`responseEnd` تحمل التأخير كله. لذلك قسنا الزمن حتى أول بايت من Node.js بدون تبطيء، واستخدمنا في Chrome لحظة اكتمال الـ HTML بدلاً منه. والدرس العام أن أدوات المحاكاة مفيدة للمقارنة بين الحالات، ولكن الأرقام المطلقة تؤخذ من القياس الحقيقي على جهاز وشبكة حقيقيين.

والمشكلة الثالثة أن صفحة CSR أظهرت في البداية أنها صارت تفاعلية قبل أول رسم للمحتوى، وهذا غير منطقي. والسبب أن الكود كان يسجل `__ready` داخل `requestAnimationFrame`، وهذا يعمل قبل خطوة الرسم في نفس الإطار كما شرحنا في مقال حلقة الأحداث، والرسم نفسه لمئتي بطاقة مع تبطيء المعالج أخذ مئات الملي ثوان. فغيرناه إلى `setTimeout` داخل `requestAnimationFrame`، حتى يسجل بعد أن يرسم الإطار فعلاً.

### ماذا يحدث إذا صارت قاعدة البيانات Database أبطأ؟

شغلنا نسخة ثانية من السيرفر على المنفذ 19391 بـ `DB_MS=1000`، أي أن كل استعلام يأخذ ثانية كاملة، وهذا يحدث في الواقع مع استعلام بدون فهرس أو قاعدة بيانات مزدحمة:

```text
== time to first byte, no throttling, median of 7 requests
csr: 2 ms
ssr: 1010 ms
ssg: 3 ms

== 5 throttled loads each (150 ms RTT, 1.6 Mbps, CPU 4x), medians in ms
csr: HTML received 174, FCP 2092, LCP 2092 (<p>), interactive 1923, API done 1883 | HTML 915 B over the wire (979 B raw), JS 68697 B, items 200, hydration errors 0
ssr: HTML received 1051, FCP 1248, LCP 1248 (<p>), interactive 2009 | HTML 5203 B over the wire (88054 B raw), JS 68757 B, items 200, hydration errors 0
ssg: HTML received 190, FCP 432, LCP 432 (<p>), interactive 1173 | HTML 5203 B over the wire (88054 B raw), JS 68757 B, items 200, hydration errors 0
```

صار الزمن حتى أول بايت في SSR 1010 ms، وأول رسم 1248 ms، وتأخر CSR أيضاً إلى 2092 ms لأن الـ API نفسه صار أبطأ. أما SSG فلم يتغير إطلاقاً، 432 ms، لأن قاعدة البيانات لا تعمل أثناء الطلب أصلاً. ولاحظ أن CSR صار تفاعلياً في 1923 ms قبل أن يسجل أول رسم في 2092 ms، وهذا ليس تناقضاً، وإنما هو الفرق بين لحظة رسم الإطار ولحظة عرضه على الشاشة تحت ضغط المعالج المبطأ. والدرس المهم هنا أن SSR يجعل صفحتك بسرعة أبطأ استعلام فيها، فكل استعلام بطيء يظهر للمستخدم كشاشة بيضاء قبل أول بايت، وهذا هو سبب انتشار الإرسال المتدفق والتخزين المؤقت أمام SSR.

![مقارنة بقاعدة بيانات 200 ms و1000 ms: CSR من FCP 1392 إلى 2092، وSSR من FCP 480 إلى 1248 مع زمن أول بايت من 217 إلى 1010، وSSG ثابت عند 440 و432، مع ثلاث بطاقات تشرح السبب لكل طريقة](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-04-slow-db.webp)

SSR يأخذ سرعة أبطأ استعلام، وSSG لا يتأثر لأن الاستعلام حدث وقت البناء

### ماذا يحدث إذا نقر المستخدم قبل الإحياء Hydration؟

الفترة التي تكون فيها الصفحة ظاهرة والأزرار لا تعمل ليست نظرية، فالسكربت `whatif-click.mjs` يحمل كل صفحة بنفس التبطيء، وينتظر حتى يظهر أول زر «أضف إلى السلة» في الـ DOM، ثم ينقر عليه بالماوس عبر بروتوكول DevTools، ثم ينتظر انتهاء الإحياء ويقرأ عداد السلة. والمخرج سوف يكون كما يلي:

```text
== ssr
  clicked at 1231 ms, ready at 1950 ms, السلة: 0
  clicked at 936 ms, ready at 1727 ms, السلة: 0
  clicked at 880 ms, ready at 1486 ms, السلة: 0
  clicked at 652 ms, ready at 1237 ms, السلة: 0
  clicked at 400 ms, ready at 1078 ms, السلة: 0
== ssg
  clicked at 505 ms, ready at 1316 ms, السلة: 0
  clicked at 773 ms, ready at 1350 ms, السلة: 0
  clicked at 412 ms, ready at 974 ms, السلة: 0
  clicked at 354 ms, ready at 1022 ms, السلة: 0
  clicked at 729 ms, ready at 1340 ms, السلة: 0
== csr
  clicked at 1502 ms, ready at 1498 ms, السلة: 1
  clicked at 1583 ms, ready at 1151 ms, السلة: 1
  clicked at 1731 ms, ready at 1288 ms, السلة: 1
  clicked at 1591 ms, ready at 1182 ms, السلة: 1
  clicked at 1831 ms, ready at 1513 ms, السلة: 1
```

في كل المحاولات العشر على SSR وSSG ضاعت النقرة، وبقيت السلة صفراً، لأن الزر كان HTML فقط عندما نقر عليه المستخدم، ولم يكن عليه أي معالج. ولم يظهر أي خطأ، ولم يحدث أي شيء على الشاشة، والمستخدم في الواقع سوف يظن أن الموقع معطل وينقر مرة ثانية. أما في CSR فلم يظهر الزر أصلاً إلا بعد أن صار جاهزاً، فكل نقرة حسبت. أي أن CSR أبطأ في الظهور، ولكنه صادق: ما تراه يعمل.

![أربع خطوات للإحياء: يصل HTML فيه الأزرار بدون معالجات، ثم ينزل JavaScript بحجم 68.7 KB ويعمل على الخيط الرئيسي، ثم يبني hydrateRoot شجرة React ويطابقها ويربط الأحداث، ثم تعمل النقرة، وبجانبها جدول من التجربة: نقرات SSR عند 400 و936 ms وSSG عند 354 و773 ms والسلة صفر، ونقرات CSR عند 1502 و1583 ms والسلة واحد](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-03-hydration.webp)

نقرة قبل الإحياء تضيع بصمت، وهذه هي الفترة التي تحتاج أن تقصرها

وكيف تقصر هذه الفترة؟ الطريقة الأولى أن تقلل الـ JavaScript التي تحتاج إحياء، فإذا كان أغلب الصفحة نصاً ثابتاً فلا داعي لإحيائه أصلاً، وهذه فكرة الجزر Islands التي تبنى عليها أدوات مثل [Astro](https://docs.astro.build/en/concepts/islands/?ref=arabroot.io): الصفحة HTML ثابتة، وفيها جزر صغيرة تفاعلية مثل زر السلة، وكل جزيرة تحيا وحدها. والطريقة الثانية أن تجعل الأزرار المهمة تعمل بدون JavaScript، فزر «أضف إلى السلة» داخل `<form method="post">` يعمل كنموذج عادي قبل الإحياء، ثم يتحول إلى طلب في الخلفية بعده، وهذا ما يسمى التحسين التدريجي Progressive Enhancement. والطريقة الثالثة أن تعطي المستخدم إشارة بأن الصفحة ما زالت تحمل، مثل أزرار باهتة حتى ينتهي الإحياء، بدلاً من أن تبدو جاهزة وهي ليست كذلك.

## محركات البحث ومعاينة الروابط: ماذا يرى الزاحف Crawler؟

قد يتساءل البعض: أليس Google يشغل JavaScript اليوم؟ والإجابة نعم، فبحسب [دليل أساسيات JavaScript SEO](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics?ref=arabroot.io) يمر Googlebot بثلاث مراحل: الزحف Crawling، ثم الرسم Rendering بنسخة حديثة من Chromium، ثم الفهرسة Indexing، وكل صفحة ترجع 200 تدخل طابور الرسم. ولكن نفس الدليل يقول إن الرسم في السيرفر أو الرسم المسبق ما زال فكرة ممتازة، لأنه يجعل الموقع أسرع للمستخدمين وللزواحف، ولأن ليس كل الزواحف تستطيع تشغيل JavaScript. وفي صفحة [الرسم الديناميكي Dynamic Rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering?ref=arabroot.io)، وهو أن ترسل للزواحف نسخة مرسومة وللمستخدمين نسخة CSR، تصفه Google بأنه حل مؤقت لقيود JavaScript وليس حلاً تنصح به على المدى الطويل، وتنصح بدلاً منه بالرسم في السيرفر أو التوليد المسبق.

وخارج Google الصورة أوضح، فبرامج معاينة الروابط في تطبيقات المحادثة ومنصات التواصل، وكثير من محركات البحث الأخرى وأدوات الذكاء الاصطناعي التي تقرأ الصفحات، تقرأ الـ HTML الأول فقط كما فعل السكربت في الخطوة الأولى، فترى في صفحة CSR 979 بايت بدون محتوى. لذلك فإن أي صفحة تريد أن تظهر في البحث أو تشارك كرابط يجب أن يكون عنوانها ووصفها ووسوم Open Graph ومحتواها الأساسي في الـ HTML الأول. وقد شرحنا كيف تقرأ المحركات الصفحة في [مقال محركات البحث من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-seo-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)، وطبقنا الإصلاحات على موقع عربي في [مقال SEO التقني للمطورين](https://arabroot.io/articles/seo-%D8%A7%D9%84%D8%AA%D9%82%D9%86%D9%8A-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D9%88%D9%82%D8%B9-%D8%B9%D8%B1%D8%A8%D9%8A-%D9%82%D8%A8%D9%84-%D8%A7%D9%84%D8%A7%D8%B5%D9%84%D8%A7%D8%AD-%D9%88%D8%A8%D8%B9%D8%AF%D9%87/).

## التكلفة على السيرفر: من يدفع ثمن الرسم Rendering؟

في CSR يدفع جهاز المستخدم ثمن الرسم، والسيرفر يقدم ملفات ثابتة وAPI فقط. وفي SSG يدفع خادم البناء الثمن مرة واحدة، ثم تقدم الملفات من CDN بتكلفة شبه معدومة، وقد شرحنا كيف تعمل شبكات توصيل المحتوى في [مقال CDN](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/). أما في SSR فيدفع سيرفرك الثمن مع كل طلب: استعلام قاعدة البيانات، وتشغيل المكونات، والضغط. وفي تجربتنا كان الرسم 2 إلى 22 ms لصفحة صغيرة في نسخة الإنتاج، ولكن هذا على معالج واحد ولطلب واحد، فإذا جاءك ألف طلب في الثانية فأنت تحتاج سيرفرات تتحمل ألف رسم في الثانية، وكل رسم يحجز حلقة الأحداث في Node.js طوال مدته كما شرحنا في مقال حلقة الأحداث، أي أن صفحة ثقيلة الرسم تؤخر كل الطلبات الأخرى على نفس العملية.

لذلك فإن SSR في الإنتاج يأتي غالباً مع تخزين مؤقت Cache، فإذا كانت صفحة التصنيف نفسها لكل الزوار فلا داعي لرسمها لكل طلب، ويمكن أن يحفظها Reverse Proxy أو CDN لمدة دقيقة، كما في [مقال التخزين المؤقت أمام موقعك](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D8%A4%D9%82%D8%AA-%D8%A7%D9%85%D8%A7%D9%85-%D9%85%D9%88%D9%82%D8%B9%D9%83-varnish-%D8%A7%D9%85-nginx/). وهذه الفكرة نفسها هي ما تقدمه الأطر الحديثة باسم إعادة التوليد التدريجي Incremental Static Regeneration، واختصاراً ISR، كما في [توثيق Next.js](https://nextjs.org/docs/app/guides/incremental-static-regeneration?ref=arabroot.io): الصفحة تبنى كملف ثابت، ثم يعاد توليدها في الخلفية بعد مدة محددة أو عند تغير البيانات، فتحصل على سرعة SSG مع بيانات لا يتجاوز عمرها دقائق. وانتبه هنا لأمر مهم: أي صفحة تحفظها في Cache مشترك يجب ألا يكون فيها شيء خاص بمستخدم معين، مثل اسمه أو سلته، وإلا ظهرت بيانات عميل لعميل آخر.

## ماذا عن التنقل Navigation بعد الصفحة الأولى؟

كل ما قسناه حتى الآن هو أول زيارة، والصورة تتغير بعدها. ففي CSR يكون الكود كله في المتصفح بعد الصفحة الأولى، فعندما ينتقل المستخدم من تصنيف القهوة إلى تصنيف التمور لا يحتاج إلا طلب API واحداً، والإطار يرسم الصفحة الجديدة دون تحميل كامل، ولذلك تبدو التطبيقات أحادية الصفحة سريعة جداً في الاستخدام المتواصل مثل لوحات التحكم. أما في SSR التقليدي فكل رابط يعني طلب HTML جديداً كاملاً، ثم تنزيل الـ JavaScript من الـ Cache، ثم إحياء جديد.

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

وانتبه إلى أن المتصفح نفسه صار يقلل الفرق في التنقل بين صفحات السيرفر، فالـ Cache يحفظ ملفات JavaScript بعد أول زيارة، وما يسمى الـ Back/Forward Cache يعيد الصفحة السابقة فوراً عند الرجوع، وقد شرحنا ترويسات التخزين المؤقت في [مقال HTTP](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/). لذلك لا تختر CSR لموقع محتوى بحجة سرعة التنقل وحدها، فالزائر الذي يقرأ مقالة واحدة ثم يغادر لا يستفيد من سرعة التنقل شيئاً، ويدفع ثمن الشاشة البيضاء كاملاً.

## قياس المختبر Lab Data وقياس المستخدمين الحقيقيين Field Data

الأرقام في هذا المقال قياس مختبر Lab Data: جهاز واحد، وشبكة محاكاة، وصفحة واحدة، وهي ممتازة للمقارنة بين الطرق لأن كل شيء آخر ثابت. ولكن ما يحكم على موقعك فعلاً هو قياس المستخدمين الحقيقيين Field Data، أي FCP وLCP وINP كما حدثت في أجهزة زوارك وشبكاتهم، وهذه تختلف كثيراً، فزائر على ألياف بصرية بجهاز حديث وزائر على شبكة جوال مزدحمة بجهاز عمره خمس سنوات يعيشان صفحتين مختلفتين. ولذلك يقيس فريق Chrome المقاييس عند الشريحة 75% وليس عند المتوسط، حتى لا يخفي الزوار السريعون معاناة الآخرين.

والطريقة العملية أن تضيف إلى الصفحة مكتبة صغيرة تقرأ هذه المقاييس من المتصفح وترسلها إلى خدمة التحليلات عندك، ثم تقارن الطرق على صفحاتك الحقيقية: صفحة تصنيف بـ SSR مقابل نفس الصفحة بـ CSR لفترة قصيرة مثلاً. وكثير من الفرق تكتشف عندها أن الفرق الأكبر لا يأتي من طريقة الرسم، وإنما من صورة كبيرة بلا ضغط، أو من خط عربي يحمل بدون `font-display` كما شرحنا في [مقال CSS للواجهات العربية](https://arabroot.io/articles/css-%D9%84%D9%84%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-%D8%A7%D9%84%D8%B9%D8%B1%D8%A8%D9%8A%D8%A9-%D8%A7%D9%84%D8%AE%D8%B5%D8%A7%D8%A6%D8%B5-%D8%A7%D9%84%D9%85%D9%86%D8%B7%D9%82%D9%8A%D8%A9-%D9%88-rtl/)، أو من سكربت إعلانات يحجز الخيط الرئيسي. أي أن طريقة الرسم قرار مهم، ولكنها ليست القرار الوحيد في سرعة الصفحة.

## الحلول الهجينة Hybrid: لست مضطراً لاختيار واحدة

في المشاريع الحقيقية لا يختار الفريق طريقة واحدة للموقع كله، وإنما لكل صفحة، والأطر الحديثة مثل Next.js وNuxt وSvelteKit وAstro تسمح بذلك في نفس المشروع. ففي متجر القهوة والتمور مثلاً تكون صفحة «من نحن» وصفحات المدونة SSG، وصفحات التصنيفات والمنتجات SSG مع إعادة توليد كل بضع دقائق أو SSR خلف Cache، وصفحة نتائج البحث SSR لأن كل بحث مختلف، وصفحات الحساب والطلبات CSR خلف تسجيل الدخول، لأن المحركات لا تراها أصلاً والتفاعل فيها هو الأهم. والصورة التالية تلخص المنطق في سؤالين: هل المحتوى نفسه لكل الزوار؟ وكم مرة يتغير؟

![شبكة من أربع خانات: محتوى واحد لكل الزوار ويتغير نادراً يعني SSG مثل التوثيق والمدونة، ومحتوى لكل مستخدم ويتغير نادراً يعني CSR خلف تسجيل الدخول مثل لوحة التحكم، ومحتوى واحد يتغير كثيراً يعني SSG مع ISR أو SSR مع Cache مثل صفحات المنتجات، ومحتوى لكل مستخدم يتغير كثيراً يعني SSR مثل نتائج البحث](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-05-choose.webp)

القرار لكل صفحة، وليس للموقع كله

## أخطاء شائعة Common Mistakes

- **تشغيل SSR بنسخة التطوير.** بدون `NODE_ENV=production` كان الرسم في تجربتنا أبطأ بأكثر من عشر مرات. تأكد من المتغير في ملف Docker والـ Compose وخدمة النظام.
- **اختيار CSR لصفحات عامة تحتاج البحث والمشاركة.** المعاينة والزواحف البسيطة ترى غلافاً فارغاً. الصفحات العامة تحتاج SSR أو SSG.
- **إضافة العنوان ووسوم Open Graph بالـ JavaScript.** حتى في صفحة مرسومة في السيرفر، إذا أضيفت هذه الوسوم بعد التحميل فلن تراها برامج المعاينة. اكتبها في الـ HTML من السيرفر.
- **اعتبار ظهور الصفحة نهاية العمل.** في التجربة ضاعت كل نقرة قبل الإحياء. قس متى تعمل الأزرار، وليس فقط متى تظهر.
- **مكونات ترسم شيئاً مختلفاً في السيرفر والمتصفح.** التاريخ الحالي، والأرقام العشوائية، و`localStorage`، والمنطقة الزمنية، كلها تسبب عدم تطابق في الإحياء. اعرض هذه القيم بعد الإحياء، أو مرر نفس القيمة من السيرفر.
- **رسم صفحة في SSR تنتظر استعلاماً بطيئاً.** كل ملي ثانية في الاستعلام تضاف إلى الزمن حتى أول بايت. استخدم الإرسال المتدفق، أو Cache، أو انقل الجزء البطيء إلى طلب بعد التحميل.
- **حفظ صفحة فيها بيانات المستخدم في Cache مشترك.** هذا يعرض بيانات عميل لعميل آخر. افصل الأجزاء الشخصية، أو اجعل ترويسة `Cache-Control` خاصة `private`.
- **كتابة البيانات في الصفحة بدون تهريب.** `JSON.stringify` وحده داخل وسم `<script>` يسمح لنص فيه `</script>` بحقن كود. استبدل `<` كما في التجربة، أو استخدم دالة الإطار المخصصة لذلك.

## جدول قرار Decision Table وقائمة تحقق Checklist

| الطريقة                    | أول رسم في التجربة | الزمن حتى أول بايت              | يرى الزاحف المحتوى | تكلفة السيرفر  | البيانات              | تناسب                                                |
| -------------------------- | ------------------ | ------------------------------- | ------------------ | -------------- | --------------------- | ---------------------------------------------------- |
| CSR                        | 1392 ms            | 2 ms                            | لا، 979 بايت فقط   | منخفضة         | حية دائماً            | الصفحات خلف تسجيل الدخول والأدوات التفاعلية          |
| SSR                        | 480 ms             | 217 ms، ويزيد مع قاعدة البيانات | نعم                | مع كل طلب      | حية مع كل طلب         | المحتوى الشخصي أو المتغير الذي يحتاج البحث والمشاركة |
| SSG                        | 440 ms             | 5 ms                            | نعم                | مرة وقت البناء | عمرها من عمر آخر بناء | المحتوى العام الذي يتغير نادراً                      |
| SSG مع ISR أو SSR مع Cache | قريب من SSG        | قريب من SSG إلا عند التجديد     | نعم                | مرة كل مدة     | عمرها دقائق           | صفحات المنتجات والتصنيفات                            |

وقبل أن تطلق صفحة عامة راجع النقاط التالية:

- افتح الصفحة بـ `curl`، هل فيها العنوان والوصف ووسوم Open Graph والمحتوى الأساسي؟
- إذا كانت SSR، هل يعمل السيرفر بنسخة الإنتاج، وكم الزمن حتى أول بايت عند أبطأ استعلام فيها؟
- كم الفترة بين أول رسم وانتهاء الإحياء على جوال متوسط، وماذا يحدث إذا نقر المستخدم فيها؟
- هل يظهر تحذير عدم تطابق في Console المتصفح؟
- هل الصفحات المحفوظة في Cache خالية من أي بيانات خاصة بمستخدم؟
- هل LCP أقل من 2.5 ثانية في الشريحة 75% من الزيارات الحقيقية، وليس فقط على جهازك؟

## تدريب عملي Practice

- أضف إلى المكون سطراً يعرض `new Date().toLocaleTimeString("ar")`، وشغل SSR، وافتح Console لترى رسالة عدم التطابق، ثم أصلحها بعرض الوقت بعد الإحياء فقط.
- غير SSR في `server.mjs` إلى `renderToPipeableStream`، وأرسل رأس الصفحة قبل انتظار قاعدة البيانات، ثم قس الزمن حتى أول بايت و FCP بـ `DB_MS=1000`.
- لف زر «أضف إلى السلة» بـ `<form method="post" action="/cart">`، وأضف المسار في السيرفر، ثم شغل `whatif-click.mjs` من جديد، وتحقق هل ما زالت النقرات تضيع.
- أضف ترويسة `Cache-Control: public, max-age=60` لمسار `/ssr` وضع أمامه Nginx مع Cache، وقس الزمن حتى أول بايت للطلب الثاني.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- CSR يرسم في المتصفح بعد تنزيل الكود والبيانات، وSSR يرسم في السيرفر مع كل طلب، وSSG يرسم مرة وقت البناء، والطريقتان الأخيرتان تحتاجان الإحياء حتى تعمل الأزرار.
- على شبكة بطيئة ظهر المحتوى في 1392 ms مع CSR، و480 مع SSR، و440 مع SSG، ولكن الصفحات الثلاث صارت تفاعلية بين 1160 و1483 ms لأنها تحتاج نفس الـ JavaScript.
- كل نقرة قبل الإحياء في SSR وSSG ضاعت بصمت في عشر محاولات من عشر، فقس متى تعمل الصفحة وليس فقط متى تظهر.
- SSR يأخذ سرعة أبطأ استعلام: بقاعدة بيانات تأخذ ثانية صار الزمن حتى أول بايت 1010 ms، وSSG لم يتأثر.
- صفحة CSR فيها 979 بايت بدون محتوى لأي برنامج لا يشغل JavaScript، والصفحات العامة تحتاج محتواها في الـ HTML الأول.
- تشغيل SSR بدون `NODE_ENV=production` جعل الرسم أبطأ بأكثر من عشر مرات، وهذا من أسهل الأخطاء وصولاً إلى الإنتاج.

وفي مرحلة [هندسة الواجهات وتطبيقات الويب](https://arabroot.io/start/#stage-web-frontend) تجد المقالات التي تكمل هذا المقال:

[![](https://arabroot.io/content/images/2026/10/browser-internals-dom-cover-cover.webp)دليلكيف يرسم المتصفح صفحتك؟ من HTML إلى البكسلاتماذا يفعل المتصفح بين وصول HTML وظهور الصفحة، ولماذا تتجمد صفحة فيها ألفا صف، وكيف تقيس كل مرحلة في Chrome وتصلحها.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84-dom-%D9%88%D8%A7%D9%84%D8%AA%D8%AE%D8%B7%D9%8A%D8%B7-%D9%88%D8%A7%D9%84%D8%B1%D8%B3%D9%85/)[![](https://arabroot.io/content/images/2026/10/js-event-loop-cover-cover.webp)دليلحلقة الأحداث Event Loop: لماذا تتأخر النقرة في صفحتك؟كيف ترتب JavaScript المهام والوعود والرسم على خيط واحد، ولماذا تنتظر النقرة خلف مهمة طويلة، وكيف تقسم العمل لتبقى الصفحة سريعة.](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/)[![](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-cover-cover-1.webp)دليلReact و Vue و Svelte: إدارة الحالة State وكلفة كل نقرةبنينا نفس التطبيق العربي بألف منتج في React وVue وSvelte وقسنا حجم الحزمة وعدد مرات إعادة الرسم مع كل نقرة، لتعرف أين تضع الحالة وكيف تختار الإطار.](https://arabroot.io/articles/react-%D9%88-vue-%D9%88-svelte-%D9%88%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AD%D8%A7%D9%84%D8%A9-state-management/)[![](https://arabroot.io/content/images/2026/10/browser-storage-pwa-cover-cover.webp)دليلالتخزين في المتصفح وتطبيقات PWA: الحدود والعمل دون إنترنتقسنا في Chrome حدود الكوكيز وlocalStorage وIndexedDB وCache API حتى رفض المتصفح، ثم شغلنا تطبيقاً والسيرفر متوقف ونشرنا إصداراً جديداً لنرى متى يصل.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D9%81%D9%8A-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD-%D9%88%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D9%88%D9%8A%D8%A8-%D8%A7%D9%84%D8%AA%D9%82%D8%AF%D9%85%D9%8A%D8%A9-pwa/)[![](https://arabroot.io/content/images/2026/10/seo-basics-cover-cover.webp)دليلكيف تعمل محركات البحث؟ SEO من الداخل للمطورينماذا يحدث لصفحتك من الزحف إلى الفهرسة إلى الترتيب، مع زاحف صغير يبين أين تتوقف صفحات موقع شركة ناشئة وما يملكه المطور لإصلاحه.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D9%85%D8%AD%D8%B1%D9%83%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-seo-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)[![](https://arabroot.io/content/images/2026/10/cdn-basics-cover-cover.webp)دليلكيف تعمل شبكة توصيل المحتوى CDN وما الذي تحفظهلماذا يبطؤ موقعك على الزائر البعيد، وكيف تقرب شبكة CDN ملفاتك منه، وكيف تمنعها من حفظ الصفحات الخاصة.](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AA%D9%88%D8%B5%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%AD%D8%AA%D9%88%D9%89-cdn-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة بـ React 19.3.1 وVite 8.3.5 وNode.js v22.14.0، والقياس في Chrome 156.0.8078.49 بدون واجهة مع تبطيء الشبكة والمعالج.