> ## 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.

# React و Vue و Svelte: إدارة الحالة State وكلفة كل نقرة
- URL: https://arabroot.io/articles/react-و-vue-و-svelte-وادارة-الحالة-state-management/
- Published: 2026-10-30T18:05:36.000Z
- Updated: 2026-10-30T20:29:26.000Z
- Description: بنينا نفس التطبيق العربي بألف منتج في React وVue وSvelte وقسنا حجم الحزمة وعدد مرات إعادة الرسم مع كل نقرة، لتعرف أين تضع الحالة وكيف تختار الإطار.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, تطوير الواجهات والجوال Frontend & Mobile

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

وهذه المشكلة لها اسم، وهو مزامنة الواجهة مع الحالة State، أي أن نفس المعلومة (محتوى السلة) معروضة في عدة أماكن، وكل مكان يحدثه كود مختلف بيده. وهذه بالضبط هي المشكلة التي جاءت أطر الواجهات Frontend Frameworks لحلها، فبدلاً من أن تكتب «غير هذا العنصر ثم ذاك»، تصف شكل الواجهة لكل قيمة من قيم الحالة، والإطار هو الذي يحدث الشاشة. ولكن الأطر الثلاثة الأشهر اليوم، React وVue وSvelte، تفعل هذا بثلاث طرق مختلفة من الداخل، ولكل طريقة ثمن في حجم الكود الذي ينزله المستخدم وفي العمل الذي يقوم به المتصفح مع كل نقرة. وهذا المقال من مرحلة [هندسة الواجهات وتطبيقات الويب Frontend](https://arabroot.io/start/#stage-web-frontend)، وسوف نبني فيه نفس التطبيق الصغير بالأطر الثلاثة، ونقيس حجم الحزمة وعدد مرات إعادة الرسم Re-render والزمن، بدلاً من الاعتماد على الانطباعات.

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

- لماذا ظهرت الأطر، وما معنى أن تكون الواجهة دالة في الحالة.
- المكونات Components والخصائص Props والحالة State، وهي المفاهيم المشتركة بين الأطر الثلاثة.
- كيف يحدث كل إطار الشاشة من الداخل: الشجرة الافتراضية Virtual DOM في React، والتفاعلية Reactivity في Vue، والمترجم Compiler والإشارات Signals في Svelte.
- إدارة الحالة State Management: الحالة المحلية والمرفوعة والمخزن المشترك، ومتى تحتاج كلاً منها.
- التجربة العملية: تطبيق عربي بألف منتج بخمس نسخ، مع حجم الحزمة وعدد مرات إعادة الرسم وتعديلات الـ DOM والزمن في Chrome.
- تكلفة JavaScript عند التحميل، وكيف تختار الإطار، والأخطاء الشائعة، وجدول قرار.

## لماذا ظهرت أطر الواجهات Frameworks؟

في الطريقة المباشرة، والتي تسمى البرمجة الأمرية Imperative، أنت تكتب الخطوات التي تغير الشاشة: ابحث عن العنصر، غير نصه، أضف صفاً، احذف صفاً. وفي صفحة صغيرة هذا كاف ومفهوم، ولكن عدد الخطوات يكبر بسرعة، لأن كل حدث يجب أن يعرف كل الأماكن التي تتأثر به. فإذا كان في الصفحة خمسة أماكن تعرض شيئاً من السلة، وأربعة أحداث تغير السلة، فأنت تحتاج عشرين تحديثاً يدوياً، وكل تحديث تنساه يصبح خطأ من نوع «السلة تقول ثلاثاً والقائمة تقول اثنتين». وأضف إلى ذلك أن كل تحديث يدوي للـ DOM قد يجبر المتصفح على حساب التخطيط Layout من جديد كما شرحنا في [مقال عمل المتصفح من الداخل](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/).

والفكرة التي غيرت هذا هي البرمجة التصريحية Declarative، وأشهر صياغة لها أن الواجهة دالة في الحالة، أي `UI = f(state)`. فأنت تكتب مرة واحدة: «رأس الصفحة يعرض مجموع الكميات في السلة»، و«كل صف يعرض كمية منتجه في السلة»، ثم تغير السلة فقط، والإطار يتكفل بأن تعرض الشاشة الحالة الجديدة في كل مكان. وقد شرح فريق React هذا الهدف عند إطلاق المكتبة في 2013 في مقال [Why did we build React?](https://legacy.reactjs.org/blog/2013/06/05/why-react.html?ref=arabroot.io)، حيث كانت المشكلة عندهم بالضبط أن البيانات تتغير مع الوقت، وأن تحديث الواجهة يدوياً لكل تغيير مصدر دائم للأخطاء.

ولكن «الإطار يتكفل» جملة تخفي سؤالاً كبيراً: كيف يعرف الإطار ما الذي تغير في الشاشة عندما تتغير الحالة؟ فالحل البسيط أن يعيد بناء الصفحة كلها مع كل تغيير، وهذا بطيء جداً ويضيع ما كتبه المستخدم في الحقول وموضع المؤشر. والإجابة عن هذا السؤال هي الفرق الحقيقي بين React وVue وSvelte، وهي ما سوف نقيسه في التجربة.

## المكونات Components والخصائص Props والحالة State

قبل الفروق، المفاهيم المشتركة. المكون Component هو قطعة من الواجهة لها اسم، مثل `ProductRow` لصف المنتج و`Header` لرأس الصفحة، وتكتب مرة وتستخدم ألف مرة. والخصائص Props هي المدخلات التي يرسلها المكون الأب إلى الابن، مثل المنتج الذي يعرضه الصف، والابن لا يغيرها. أما الحالة State فهي البيانات التي تتغير مع الوقت بسبب المستخدم أو الشبكة، مثل محتوى السلة ونص البحث، والقاعدة أن كل تغيير في الشاشة يبدأ من تغيير في الحالة.

ويوجد في الأطر الثلاثة شيء رابع مهم، وهو المفتاح Key الذي تعطيه لكل عنصر في قائمة، مثل رقم المنتج. فعندما تتغير القائمة بعد البحث يستخدم الإطار المفتاح ليعرف أن الصف رقم 37 ما زال موجوداً فيبقيه كما هو، وأن الصف رقم 40 اختفى فيحذفه، بدلاً من أن يعيد كتابة كل الصفوف حسب ترتيبها. ونسيان المفتاح، أو استخدام ترتيب العنصر في المصفوفة كمفتاح، من أشهر أخطاء القوائم، وسوف نعود إليه في قسم الأخطاء.

## كيف يحدث React الشاشة؟ الشجرة الافتراضية Virtual DOM والمقارنة Reconciliation

المكون في React دالة JavaScript عادية ترجع وصفاً للواجهة، وعندما تتغير حالة مكون بـ `setState` يقوم React بثلاث خطوات يشرحها دليل [Render and Commit](https://react.dev/learn/render-and-commit?ref=arabroot.io). الأولى أنه يعيد تشغيل دالة المكون الذي تغيرت حالته، ثم دالة كل مكون تحته في الشجرة، وهذا ما يسمى الرسم Render في مصطلحات React، مع أنه لا يرسم شيئاً على الشاشة بعد. والثانية أنه يقارن الوصف الجديد بالقديم، وهذه المقارنة تسمى Reconciliation، ونتيجتها قائمة صغيرة بالفروق. والثالثة أنه يطبق هذه الفروق فقط على الـ DOM الحقيقي، وهذه خطوة الاعتماد Commit.

وهذا التصميم بسيط جداً من ناحية المطور، فأنت لا تحتاج أن تخبر React بما قرأه كل مكون من الحالة، لأن React لا يتتبع ذلك أصلاً، وإنما يعيد التشغيل ويقارن. ولكن ثمنه واضح: إذا كانت السلة حالة في المكون الأعلى `App`، فإن إضافة قطعة واحدة تعيد تشغيل `App` وكل ما تحته، أي ألف دالة `ProductRow` في تطبيقنا، حتى لو تغير صف واحد فقط على الشاشة. والمقارنة سريعة عادة، ولكنها ليست مجانية، والتجربة سوف تريك كم تكلف على جهاز بطيء.

### memo و React Compiler: كيف توقف إعادة الرسم غير اللازمة؟

الأداة التقليدية لهذه المشكلة هي [memo](https://react.dev/reference/react/memo?ref=arabroot.io)، فعندما تلف بها مكوناً فإن React يقارن خصائصه الجديدة بالقديمة قبل أن يعيد تشغيله، وإذا لم يتغير شيء يتخطاه. ولكن هذا يعمل فقط إذا صممت الخصائص بعناية، فإذا أرسلت إلى كل صف كائن السلة كاملاً فإن هذا الكائن جديد بعد كل إضافة، فيتغير عند كل الصفوف ولا يفيد `memo` شيئاً. والحل أن ترسل إلى كل صف كميته فقط، وأن تثبت الدوال التي ترسلها بـ `useCallback`، لأن الدالة التي تنشأ من جديد في كل رسم تعتبر خاصية جديدة.

وفي أكتوبر 2025 أصدر فريق React [React Compiler 1.0](https://react.dev/blog/2025/10/07/react-compiler-1?ref=arabroot.io)، وهو أداة تعمل وقت البناء وتضيف التخزين المؤقت Memoization إلى المكونات تلقائياً، بحيث لا تحتاج أن تكتب `memo` و`useMemo` بيدك. ويشرح [دليل المترجم](https://react.dev/learn/react-compiler?ref=arabroot.io) كيف تفعله. والسؤال المهم الذي سوف تجيب عنه التجربة: هل يحل المترجم مشكلة الصفوف الألف في كودنا الذي يرسل كائن السلة كاملاً إلى كل صف؟

## Vue: التفاعلية Reactivity بالـ Proxy

يأخذ Vue الطريق المعاكس، فبدلاً من أن يعيد التشغيل ثم يقارن ليكتشف ما تغير، فإنه يتتبع من قرأ ماذا. فعندما تكتب `reactive({})` يلف Vue الكائن بـ [Proxy](https://vuejs.org/guide/extras/reactivity-in-depth.html?ref=arabroot.io)، وهو ميزة في JavaScript تسمح باعتراض كل قراءة وكل كتابة على الكائن. وعندما يرسم المكون `ProductRow` للمنتج رقم 5 ويقرأ `cart[5]`، يسجل Vue أن هذا المكون بالذات يعتمد على هذا المفتاح بالذات. وعندما تكتب لاحقاً `cart[5]++` يعرف Vue فوراً أن المكون الوحيد الذي يحتاج تحديثاً هو هذا الصف، ومعه أي مكون آخر قرأ نفس القيمة، مثل رأس الصفحة الذي يجمع الكميات.

ويبقى في Vue شجرة افتراضية، ولكنها لكل مكون على حدة، فالمكون الذي تغيرت اعتماداته يعيد رسم شجرته الصغيرة ويقارنها، ولا يلمس أباه ولا إخوته، كما يشرح دليل [آلية الرسم في Vue](https://vuejs.org/guide/extras/rendering-mechanism.html?ref=arabroot.io). والمترجم الخاص بقوالب Vue يضيف علامات على الأجزاء الثابتة في القالب حتى لا يقارنها أصلاً. والثمن هنا أن التتبع نفسه يعمل وقت التشغيل، فكل كائن تفاعلي يمر عبر Proxy، وكل قراءة لها تكلفة صغيرة، وبعض الأنماط مثل تفكيك الكائن التفاعلي إلى متغيرات منفصلة تقطع التتبع دون أن تنتبه.

## Svelte: المترجم Compiler والإشارات Signals

أما Svelte فتنقل أغلب العمل إلى وقت البناء، فهي في الأساس مترجم Compiler يقرأ ملفات `.svelte` ويحولها إلى JavaScript تنشئ عناصر الـ DOM مباشرة، دون شجرة افتراضية ودون مقارنة. وقد شرح مؤسسها هذه الفكرة في مقال [Rethinking reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity?ref=arabroot.io) عام 2019\. ومنذ [الإصدار Svelte 5](https://svelte.dev/blog/svelte-5-is-alive?ref=arabroot.io) صارت التفاعلية مبنية على ما يسمى [الـ Runes](https://svelte.dev/docs/svelte/what-are-runes?ref=arabroot.io)، مثل `$state` و`$derived`، وهي في الداخل إشارات Signals: كل قيمة تفاعلية تعرف من يعتمد عليها، والمعتمد هنا ليس مكوناً كاملاً، وإنما تأثير Effect صغير مرتبط بعقدة نص واحدة أو خاصية واحدة في الـ DOM.

ولذلك فإن مفهوم «إعادة رسم المكون» غير موجود في Svelte 5 أصلاً، فالكود داخل وسم `<script>` في المكون يعمل مرة واحدة عند إنشائه، وعندما يتغير `cart[5]` يتحدث النص الذي يعرض الكمية في الصف الخامس مباشرة، ولا شيء غيره. وفكرة الإشارات ليست خاصة بـ Svelte، فهي موجودة في SolidJS وAngular وغيرها، ويوجد [مقترح في TC39](https://github.com/tc39/proposal-signals?ref=arabroot.io) لإضافتها إلى JavaScript نفسها. والثمن أن السلوك يعتمد على ما يولده المترجم، فكود Svelte لا يعمل بدون خطوة البناء، وعندما تبحث عن خطأ فأنت تقرأ أحياناً كوداً كتبه المترجم وليس أنت.

![ثلاثة أعمدة: React حيث يعلم setCart المكون App ثم تعاد كل دوال المكونات تحته فتظهر الشجرة كلها بالأزرق ثم تقارن وتطبق الفروق، وVue حيث يمر التغيير على Proxy فيعرف المكون الذي قرأ المفتاح ويعيد رسمه فقط فتظهر عقدة واحدة خضراء، وSvelte 5 حيث حول المترجم القالب إلى كود يوقظ تأثيراً مرتبطاً بعقدة نص واحدة فتظهر عقدة واحدة برتقالية، ولكل عمود سطر عن الثمن](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-01-three-models.webp)

ثلاث إجابات لنفس السؤال: ما الذي يجب أن يتغير على الشاشة؟

## إدارة الحالة State Management: أين تضع السلة؟

بعد أن عرفت كيف يحدث كل إطار الشاشة، يأتي السؤال الذي يأخذ أغلب وقت الفرق في المشاريع الحقيقية: أين تعيش الحالة؟ وهذا السؤال يحدد عدد المكونات التي تتأثر بكل تغيير، وبالتالي سرعة الواجهة، ويحدد أيضاً سهولة فهم الكود بعد سنة. وفيما يلي الأماكن الثلاثة، بالترتيب الذي ينصح به عادة:

- **الحالة المحلية Local State:** داخل المكون الذي يستخدمها فقط، مثل نص حقل البحث أو قائمة منسدلة مفتوحة. وهي الأبسط والأسرع، لأن تغييرها لا يصل إلى أحد غير صاحبها. والقاعدة أن تبدأ بها دائماً، ولا ترفعها إلا عندما يحتاجها مكون آخر.
- **الحالة المرفوعة Lifted State:** عندما يحتاج مكونان نفس القيمة ترفعها إلى أقرب أب مشترك، وترسلها إليهما كخصائص. وهذا ما فعلناه بالسلة في نسخة React الأولى، فالسلة في `App` وتنزل إلى الرأس وإلى كل صف. والمشكلة عندما يكون الأب بعيداً، فتمر الخاصية على مكونات لا تحتاجها فقط لتوصلها إلى الأسفل، وهذا ما يسمى Prop Drilling، وفي React يعيد كل تغيير تشغيل كل هذه الطبقات.
- **المخزن المشترك Store:** الحالة في وحدة مستقلة خارج شجرة المكونات، وكل مكون يقرأ منها ما يحتاجه مباشرة. وفي Vue هذا كائن `reactive` في ملف مستقل كما في [دليل إدارة الحالة](https://vuejs.org/guide/scaling-up/state-management.html?ref=arabroot.io)، أو مكتبة [Pinia](https://pinia.vuejs.org/?ref=arabroot.io) الرسمية للمشاريع الكبيرة، وفي Svelte ملف `.svelte.js` فيه `$state`، وفي React الـ [Context مع useReducer](https://react.dev/learn/scaling-up-with-reducer-and-context?ref=arabroot.io) أو مكتبة خارجية تعتمد على [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore?ref=arabroot.io) حتى يشترك كل مكون في الجزء الذي يقرؤه فقط.

![ثلاثة أعمدة: الحالة المحلية في مكون البحث وحده مع ميزة أن التغيير لا يصل إلى غيره، والحالة المرفوعة في App تنزل إلى Header وProductList ثم إلى ألف صف مع عيب أن React يعيد تشغيل الشجرة كلها، والمخزن المشترك خارج الشجرة يشترك فيه الرأس والصف الخامس مباشرة بخطوط متقطعة](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-04-state-placement.webp)

كلما ارتفعت الحالة زاد من يتأثر بتغييرها، والمخزن يسمح لكل مكون أن يشترك فيما يقرؤه فقط

وقد يتساءل البعض: لماذا لا أضع كل الحالة في مخزن عام واحد من البداية وأرتاح؟ والإجابة أن المخزن العام يستطيع أي ملف في المشروع أن يكتب فيه، فعندما تجد قيمة خاطئة في السلة لا تعرف أي مكون غيرها، وهذا بالضبط ما جاءت الأطر لتمنعه. لذلك ضع في المخزن فقط ما يشترك فيه عدد من الصفحات فعلاً، مثل السلة وبيانات المستخدم، واجعل الكتابة فيه عبر دوال محددة مثل `add` و`remove` بدلاً من التعديل المباشر من أي مكان. وانتبه لنوع ثالث من البيانات كثيراً ما يخلط بالحالة، وهو بيانات السيرفر مثل قائمة المنتجات، فهذه ليست حالة يملكها كودك، وإنما نسخة مؤقتة من بيانات في قاعدة البيانات، وتحتاج تحديثاً وإلغاء وإعادة محاولة، والأنسب لها مكتبات جلب البيانات المتخصصة، وليس المخزن الذي تضع فيه السلة.

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

الآن نقيس كل ما سبق. التطبيق صفحة واحدة فيها رأس يعرض عدد القطع في السلة، وحقل بحث، وقائمة بألف منتج عربي مثل «قهوة عربية 250 غ رقم 1» و«عسل سدر 1 كغ رقم 23»، ولكل منتج زر «أضف». وبنينا خمس نسخ:

1. **React:** الطريقة المباشرة التي يكتبها أغلب المطورين أول مرة، فالسلة ونص البحث حالة في `App`، والسلة كاملة تنزل إلى كل صف.
2. **React + memo:** نفس الكود مع الإصلاحين المعروفين: كل صف يأخذ كميته فقط، والصف ملفوف بـ `memo`، والدالة `add` ثابتة بـ `useCallback`.
3. **React Compiler:** نفس كود النسخة الأولى حرفياً، بدون أي `memo`، ولكنه مبني عبر React Compiler 1.0.
4. **Vue 3.5:** السلة في مخزن `reactive`، وكل صف يقرأ كميته منه مباشرة.
5. **Svelte 5:** السلة في ملف `store.svelte.js` بـ `$state`، وكل صف يقرأ كميته منه.

وكل النسخ تبنى بنفس الأداة [Vite](https://vite.dev/guide/?ref=arabroot.io) بالإصدار 8.3.5، وتقرأ نفس ملف البيانات، ونفس ملف الأنماط.

### الكود في الأطر Frameworks الثلاثة

هذا هو كود نسخة React الأولى كاملاً، ولاحظ أن كل مكون يستدعي `count` في أول سطر، وهي دالة صغيرة تزيد عداداً في `window.__renders` باسم المكون، حتى نعرف كم مرة عمل كل مكون:

```jsx
// The straightforward version: all state in App, passed down as props
import { useState } from "react";
import { products } from "../shared/data.js";
import { count } from "../shared/probe.js";

export default function App() {
  count("App");
  const [cart, setCart] = useState({});
  const [q, setQ] = useState("");
  const list = products.filter((p) => p.name.includes(q));
  const add = (id) => setCart((c) => ({ ...c, [id]: (c[id] || 0) + 1 }));
  return (
    <>
      <Header cart={cart} />
      <input placeholder="ابحث عن منتج" value={q} onChange={(e) => setQ(e.target.value)} />
      <ProductList items={list} cart={cart} onAdd={add} />
    </>
  );
}

function Header({ cart }) {
  count("Header");
  const n = Object.values(cart).reduce((a, b) => a + b, 0);
  return <header><b>السلة: <span id="cart-count">{n}</span></b></header>;
}

function ProductList({ items, cart, onAdd }) {
  count("ProductList");
  return <ul>{items.map((p) => <ProductRow key={p.id} p={p} cart={cart} onAdd={onAdd} />)}</ul>;
}

function ProductRow({ p, cart, onAdd }) {
  count("ProductRow");
  return (
    <li>
      <span className="name">{p.name}</span>
      <span>{p.price} ر.س</span>
      <span className="qty">{cart[p.id] || 0}</span>
      <button onClick={() => onAdd(p.id)}>أضف</button>
    </li>
  );
}
```

أما في Vue فالسلة في ملف مستقل، وصف المنتج يقرأ منها مباشرة:

```javascript
// vue/store.js
import { reactive } from "vue";
// Shared state: a component that reads cart[id] re-renders when that key changes, and only then
export const cart = reactive({});
export const add = (id) => { cart[id] = (cart[id] || 0) + 1; };
```

```html
<!-- vue/ProductRow.vue -->
<script setup>
import { onMounted, onUpdated } from "vue";
import { cart, add } from "./store.js";
import { count } from "../shared/probe.js";
defineProps(["p"]);
onMounted(() => count("ProductRow")); onUpdated(() => count("ProductRow"));
</script>
<template>
  <li>
    <span class="name">{{ p.name }}</span>
    <span>{{ p.price }} ر.س</span>
    <span class="qty">{{ cart[p.id] || 0 }}</span>
    <button @click="add(p.id)">أضف</button>
  </li>
</template>
```

ولاحظ أن Vue لا يملك دالة تعمل مع كل رسم مثل React، لذلك نعد في Vue مرات التركيب Mount مع مرات التحديث Update، وهذا هو المقابل الأقرب. وفي Svelte نفس الفكرة بالـ Runes:

```javascript
// svelte/store.svelte.js
// Shared state with Svelte 5 runes: reading cart[id] in a template subscribes that one text node
export const cart = $state({});
export const add = (id) => { cart[id] = (cart[id] || 0) + 1; };
```

```html
<!-- svelte/ProductRow.svelte -->
<script>
  import { cart, add } from "./store.svelte.js";
  import { count } from "../shared/probe.js";
  let { p } = $props();
  count("ProductRow");
</script>
<li>
  <span class="name">{p.name}</span>
  <span>{p.price} ر.س</span>
  <span class="qty">{cart[p.id] || 0}</span>
  <button onclick={() => add(p.id)}>أضف</button>
</li>
```

وفي Svelte يعمل `count` عند إنشاء المكون فقط، لأن كود الـ `script` لا يعمل مرة أخرى، وهذا بالضبط ما نريد أن نراه في الأرقام. وبقية الملفات، أي النسخة الثانية من React ورأس الصفحة في Vue وSvelte وملف الإعداد، موجودة مع المقال في مجلد `lab`.

### البناء وحجم الحزمة Bundle

نبني النسخ الخمس بملف إعداد واحد يختار الإطار من متغير البيئة `FW`، ونسخة المترجم تضيف `reactCompilerPreset` من حزمة `@vitejs/plugin-react` عبر `@rolldown/plugin-babel` كما في توثيق الإضافة:

```bash
npm install
for f in react react-memo react-compiler vue svelte; do FW=$f npx vite build; done
node sizes.mjs
```

والسكربت `sizes.mjs` يضغط ملف JavaScript الناتج لكل نسخة بـ gzip بالمستوى 9 وبـ brotli بأعلى جودة، كما يفعل السيرفر عادة قبل الإرسال. والمخرج سوف يكون كما يلي:

```text
react           raw  221100  gzip  68541  brotli  59005
react-memo      raw  221159  gzip  68574  brotli  59042
react-compiler  raw  222442  gzip  69195  brotli  59458
vue             raw   66964  gzip  26385  brotli  23983
svelte          raw   41997  gzip  16487  brotli  14886
```

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

- نسخ React الثلاث تقريباً بنفس الحجم، 221 KB قبل الضغط و68 KB بعد gzip، والسبب أن أغلب الحجم هو مكتبة `react-dom` نفسها وليس كود التطبيق. ومن المهم أن تلاحظ أن React Compiler أضاف 1,342 بايت فقط قبل الضغط، فهو لا يضيف مكتبة، وإنما يضيف بضعة أسطر لكل مكون.
- Vue أصغر من React بثلاث مرات تقريباً: 66,964 بايت قبل الضغط و26,385 بعد gzip.
- Svelte أصغر الجميع: 16,487 بايت بعد gzip، أي ربع React تقريباً، والسبب أن المترجم يضع في الحزمة فقط ما يستخدمه التطبيق من مكتبة التشغيل الصغيرة.
- هذه الأرقام تشمل كود التطبيق وبيانات ألف منتج المولدة في الكود، وفي تطبيق حقيقي سوف يكبر الحجم بمكتبات التوجيه Routing وجلب البيانات والنماذج، وهذه تضاف فوق حجم الإطار.

![أعمدة أفقية لحجم الحزمة: React وReact مع memo عند 221 ألف بايت قبل الضغط و68.5 ألف بعد gzip و59 ألف بعد brotli، وReact Compiler 222,442 و69,195 و59,458، وVue 66,964 و26,385 و23,983، وSvelte 41,997 و16,487 و14,886](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-02-bundle.webp)

نفس التطبيق: React أكبر بأربع مرات من Svelte بعد الضغط

### كيف نقيس إعادة الرسم Re-render والزمن؟

السكربت `measure.mjs` يشغل سيرفراً صغيراً للملفات المبنية، ويقود Chrome 156 بدون واجهة عبر بروتوكول DevTools باستخدام المساعد `scripts/lab/browser.mjs`. ولكل نسخة يحمل الصفحة، ثم يضيف إليها مجساً صغيراً يقوم بثلاثة أمور مع كل حدث نقر أو كتابة:

- يصفر عدادات المكونات، ثم يقرؤها بعد التحديث، فنعرف كم مكوناً عمل بسبب هذا الحدث فقط.
- يعد التعديلات الفعلية على الـ DOM بـ `MutationObserver`: إضافة عنصر أو حذفه، وتغيير نص، وتغيير خاصية.
- يقيس الزمن من لحظة الحدث إلى ما بعد الإطار التالي، أي `requestAnimationFrame` ثم `setTimeout`، وهو تقريب للزمن الذي ينتظره المستخدم حتى يرى النتيجة، وقد شرحنا موقع هذين في الحلقة في [مقال حلقة الأحداث](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/).

ثم ينفذ السكربت عشرين نقرة حقيقية بالماوس على أزرار «أضف»، وعشر جولات يكتب فيها حروف كلمة «عسل» واحداً بعد الآخر ثم يحدد النص ويمسحه. وكل ذلك مع تبطيء المعالج أربع مرات بالأمر [Emulation.setCPUThrottlingRate](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/?ref=arabroot.io#method-setCPUThrottlingRate)، وهو نفس خيار التبطيء في DevTools، لأن جهاز المطور أسرع بكثير من جوال متوسط عند عملائك. والتشغيل كما يلي، من المجلد الذي فيه `dist`:

```bash
node measure.mjs 4
node measure.mjs 1
```

### نقرة واحدة على «أضف»: كم مكوناً Component يعاد رسمه؟

هذا جزء النقرة من مخرج التشغيل بتبطيء 4 مرات، لكل نسخة:

```text
== react
click x20: median 92.1 ms, cart=20
  first click: 209.8 ms, mutations 4, renders App=1 Header=1 ProductList=1 ProductRow=1000
== react-memo
click x20: median 66 ms, cart=20
  first click: 17.1 ms, mutations 4, renders App=1 Header=1 ProductList=1 ProductRow=1
== react-compiler
click x20: median 39.8 ms, cart=20
  first click: 97.3 ms, mutations 2, renders App=1 Header=1 ProductList=1 ProductRow=1000
== vue
click x20: median 64.5 ms, cart=20
  first click: 53.2 ms, mutations 2, renders Header=1 ProductRow=1
== svelte
click x20: median 52.8 ms, cart=20
  first click: 36.4 ms, mutations 2, renders none
```

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

- **React المباشر أعاد تشغيل 1003 مكونات لنقرة واحدة**: `App` و`Header` و`ProductList` وألف `ProductRow`، مع أن الشاشة تغير فيها رقمان فقط. وهذا ما توقعناه، فالسلة تنزل إلى كل صف، وكل إضافة تنشئ كائن سلة جديداً.
- **React مع memo أعاد تشغيل صف واحد فقط**، ومعه الطبقات الثلاث العليا التي تمر عليها الحالة، أي أن `memo` أوقف 999 صفاً عند مقارنة الخصائص.
- **React Compiler أعاد تشغيل الألف صف كلها**، وهذه أهم ملاحظة في التجربة. فالمترجم يحفظ النتائج ما دامت المدخلات لم تتغير، ولكن المدخل هنا هو كائن السلة كاملاً، وهو تغير فعلاً بعد الإضافة، فلم يجد المترجم ما يحفظه. أي أن المترجم لا يصلح شكل البيانات الذي اخترته، وإذا أرسلت إلى كل صف كائناً يتغير مع كل نقرة فسوف يعمل كل صف مهما كانت الأداة.
- **Vue أعاد رسم مكونين فقط**: الرأس الذي يجمع الكميات، والصف الذي تغيرت كميته، ولم يعمل `App` أصلاً لأنه لا يقرأ السلة.
- **Svelte لم يعد تشغيل أي مكون**، فكلمة `none` تعني أن لا يوجد كود مكون عمل، وإنما تحدثت عقدتا النص مباشرة.
- **التعديلات على الـ DOM نفسها متقاربة**: 2 في Vue وSvelte والمترجم، و4 في نسختي React الأخريين. فكل الأطر تكتب في الـ DOM الفروق فقط، والفرق الحقيقي بينها هو العمل الذي يسبق هذه الكتابة.

أما الزمن فهو أقل الأرقام ثباتاً كما سوف نشرح في المشكلات، ولذلك شغلنا القياس مرتين بتبطيء 4 مرات، ومرة بدون تبطيء. والوسيط لعشرين نقرة كان في التشغيل الأول 87.6 ms لـ React و69.9 لـ memo و42.9 للمترجم و39.4 لـ Vue و37.9 لـ Svelte، وفي التشغيل الثاني كما في المخرج أعلاه. والنتيجة الثابتة في التشغيلين أن React المباشر هو الأبطأ بفارق واضح، أما ترتيب الأربعة الباقية فتغير بين التشغيلين، أي أن الفرق بينها أصغر من تذبذب القياس نفسه. والصورة التالية تجمع النتيجة:

![جدول لنقرة واحدة على أضف: React أعاد ألف صف مع App وHeader وProductList وأربعة تعديلات DOM وزمن 92.1 و87.6 ms بتبطيء أربع مرات و15.4 بدونه، وReact مع memo صف واحد و66.0 و69.9 و12.7، وReact Compiler ألف صف وتعديلان و39.8 و42.9 و13.7، وVue صف واحد مع Header وتعديلان و64.5 و39.4 و13.8، وSvelte صفر مكونات وتعديلان و52.8 و37.9 و11.4، مع ألف مربع صغير لصفوف React وReact Compiler](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-03-click.webp)

عدد المكونات التي عملت يختلف من صفر إلى ألف، أما تعديلات الـ DOM فمتقاربة

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

### الكتابة في البحث ومسحه

هذا جزء الكتابة من نفس التشغيل لنسختين، لأن الثلاث الأخرى بنفس الشكل:

```text
== react
type "ع": rows 530, median 259.8 ms, mutations 475, renders App=1 Header=1 ProductList=1 ProductRow=530
type "عس": rows 125, median 263.1 ms, mutations 410, renders App=1 Header=1 ProductList=1 ProductRow=125
type "عسل": rows 125, median 172.6 ms, mutations 5, renders App=1 Header=1 ProductList=1 ProductRow=125
clear: rows 1000, median 1719.3 ms, mutations 880, renders App=1 Header=1 ProductList=1 ProductRow=1000
== svelte
type "ع": rows 530, median 286 ms, mutations 470, renders none
type "عس": rows 125, median 158.9 ms, mutations 405, renders none
type "عسل": rows 125, median 39.5 ms, mutations 0, renders none
clear: rows 1000, median 1438 ms, mutations 875, renders ProductRow=875
```

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

- الحرف الأول «ع» يطابق كل منتج فيه «عربية» أو «عسل» أو «عبوة»، فبقي 530 صفاً وحذف 470، وعدد التعديلات 470 في Svelte و475 في React، أي أن الإطارين حذفا نفس الصفوف تقريباً. وكان الزمن في الإطارين متقارباً، لأن أغلب العمل هنا هو حذف العناصر من الـ DOM ثم التخطيط والرسم، وهذا عمل المتصفح وليس عمل الإطار.
- الحرف الثالث «ل» لم يغير القائمة، لأن «عس» تطابق «عسل» فقط، ومع ذلك أعاد React تشغيل 125 صفاً والرأس، وفي Svelte كانت التعديلات صفراً ولم يعمل شيء.
- المسح هو الأثقل في كل الأطر، لأن القائمة تعود من 125 صفاً إلى 1000، فيجب إنشاء 875 عنصراً جديداً مع أزرارها. ولاحظ أن Svelte أنشأ 875 مكوناً بالضبط، وهي الصفوف الجديدة فقط، بفضل المفتاح `p.id`، أما React المباشر فأعاد تشغيل الألف لأن الـ 125 الموجودة لم تكن ملفوفة بـ `memo`.

وزمن المسح كان في التشغيلين بين 1.1 و1.9 ثانية لكل الأطر بتبطيء 4 مرات، وبين 227 و248 ms بدون تبطيء، وتغير ترتيب الأطر بين التشغيلين. وهذا الدرس أهم من ترتيب الأطر نفسه: عندما تعرض ألف عنصر مرة واحدة فإن تكلفة إنشائها في الـ DOM ورسمها هي التي تحكم الزمن، ولن ينقذك تغيير الإطار. والحل هو أن تعرض أقل، بالتقسيم إلى صفحات Pagination، أو بالعرض الافتراضي Virtualization الذي ينشئ فقط الصفوف الظاهرة على الشاشة.

![أعمدة أفقية لزمن مسح البحث وعودة ألف صف: React 1719 و1564 ms بتبطيء أربع مرات و227 بدونه، وReact مع memo 1912 و1571 و246، وReact Compiler 1401 و1380 و233، وVue 1716 و1173 و248، وSvelte 1438 و1076 و243، مع ثلاث بطاقات تشرح أن العمل الأكبر هو إنشاء 875 عنصراً وأن الفرق بين الأطر صغير وأن الحل هو عرض عناصر أقل](https://arabroot.io/content/images/2026/10/frontend-frameworks-arch-05-clear.webp)

إنشاء 875 صفاً يكلف أكثر من أي فرق بين الأطر

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

المشكلة الأولى كانت في أول نسخة من السكربت، فقد كان ينتظر 250 ms بعد كل حرف، و400 ms بعد المسح، ثم يقرأ القياسات. ولكن المسح بتبطيء 4 مرات أخذ أكثر من ثانية، فبدأ الحرف التالي قبل أن ينتهي القياس السابق، واختلطت القياسات، وظهرت أرقام غير منطقية مثل صفوف لم تعد بعد المسح. والحل كان أن ينتظر السكربت حتى تسجل الصفحة نهاية القياس بنفسها، بدلاً من الانتظار مدة ثابتة. وهذا نفس الخطأ الذي يقع فيه من يكتب اختبارات الواجهة بـ `sleep` ثابت، فالاختبار ينجح على جهازك ويفشل على خادم CI الأبطأ.

والمشكلة الثانية في قياس أول رسم للمحتوى First Contentful Paint، فقد ظهرت القيمة `-1` لنسختي React في أحد التشغيلات، والسبب أن السكربت قرأ القيمة بمجرد ظهور عنصر العداد في الـ DOM، وفي تلك اللحظة لم يكن المتصفح قد رسم الإطار بعد، فلم يكن القياس موجوداً. والحل أن ينتظر السكربت ظهور القياس نفسه في `performance.getEntriesByName`.

والمشكلة الثالثة هي تذبذب الزمن، فنفس الكود في نسختي React الأولى والثانية، وهما متطابقتان عند التحميل، أعطى أول رسم في 1088 ms و1300 ms بتبطيء 4 مرات، وزمن نقرة Vue كان 39.4 ms في تشغيل و64.5 ms في آخر. والسبب أن التبطيء يضاعف كل تذبذب في الجهاز الذي يعمل عليه القياس، من عمليات أخرى ومن جمع الذاكرة Garbage Collection. لذلك اعتمدنا في الاستنتاجات على ما ثبت في كل التشغيلات: عدد المكونات والتعديلات لا يتغير أبداً، وحجم الحزمة لا يتغير، وأما الزمن فنأخذ منه ما تكرر فقط، مثل أن React المباشر أبطأ في النقرة. وإذا قارنت الأطر بنفسك فلا تحكم من تشغيل واحد، وشغل القياس عدة مرات وانظر إلى ما يبقى ثابتاً.

### ماذا يحدث على جهاز سريع؟ أثر حجم DOM

شغلنا نفس القياس بدون أي تبطيء، أي بسرعة جهاز المطور، وهذه سطور النقرة والمسح لكل نسخة:

```text
Browser: Chrome/156.0.8078.49 | CPU throttle: 1x
FCP median of 7 loads: react 280 ms, react-memo 228 ms, react-compiler 216 ms, vue 236 ms, svelte 208 ms
== react
click x20: median 15.4 ms, cart=20
clear: rows 1000, median 226.5 ms, mutations 880, renders App=1 Header=1 ProductList=1 ProductRow=1000
== react-memo
click x20: median 12.7 ms, cart=20
clear: rows 1000, median 246.3 ms, mutations 880, renders App=1 Header=1 ProductList=1 ProductRow=875
== react-compiler
click x20: median 13.7 ms, cart=20
clear: rows 1000, median 233 ms, mutations 880, renders App=1 ProductList=1 ProductRow=1000
== vue
click x20: median 13.8 ms, cart=20
clear: rows 1000, median 247.7 ms, mutations 875, renders ProductRow=875 App=1
== svelte
click x20: median 11.4 ms, cart=20
clear: rows 1000, median 243.3 ms, mutations 875, renders ProductRow=875
```

وهذه سطور مختارة من الملف `run-output-1x.txt` الموجود مع المقال، وفيه سطور الكتابة أيضاً. وعلى جهاز سريع اختفت الفروق تقريباً: كل النقرات بين 11 و16 ms، أي داخل إطار واحد، وكل عمليات المسح بين 227 و248 ms. حتى React المباشر الذي يعيد ألف صف مع كل نقرة لم يتأخر إلا بضعة أجزاء من الملي ثانية. وهذا يفسر لماذا لا يلاحظ كثير من الفرق مشكلات الأداء أثناء التطوير، ثم تظهر عند العملاء الذين يستخدمون جوالات متوسطة، فالعمل الزائد موجود دائماً، ولكنه يختفي داخل إطار واحد على الجهاز السريع. لذلك اختبر دائماً بتبطيء المعالج في DevTools، والأفضل على جوال حقيقي متوسط السعر.

## حجم JavaScript: التكلفة التي يدفعها كل زائر

الفرق في إعادة الرسم يظهر مع التفاعل، أما الفرق في حجم الحزمة فيدفعه كل زائر عند كل زيارة جديدة، حتى قبل أن يضغط أي زر. ويشرح فريق V8 في [The cost of JavaScript](https://v8.dev/blog/cost-of-javascript-2019?ref=arabroot.io) أن تكلفة JavaScript ليست في التنزيل فقط، وإنما في التحليل Parse والترجمة والتنفيذ على الخيط الرئيسي، وهذا ما يجعل 100 KB من JavaScript أثقل على الجوال من 100 KB من الصور. وفي تقرير [Web Almanac 2024](https://almanac.httparchive.org/en/2024/javascript?ref=arabroot.io) كان وسيط حجم JavaScript في صفحات الجوال 558 KB، بزيادة 14% عن السنة السابقة، وهذا يعني أن حجم الإطار جزء من ميزانية أكبر بكثير، وأن ما تضيفه فوقه من مكتبات هو الذي يحدد غالباً حجم الصفحة.

وفي تجربتنا كان وسيط أول رسم للمحتوى بتبطيء 4 مرات في سبعة تحميلات 708 ms للمترجم، و836 ms لـ Svelte، و872 ms لـ Vue، و1088 و1300 ms لنسختي React، ولكن هذه الأرقام تقيس التحميل من نفس الجهاز بدون تبطيء للشبكة، ونسختا React متطابقتان عند التحميل وأعطتا رقمين بينهما 212 ms، أي أن التذبذب هنا كبير. والذي يمكن أن تأخذه منها أن عرض ألف صف عند التحميل يأخذ معظم الوقت في كل الأطر، وأن فرق الحجم يظهر أكثر عندما تكون الشبكة بطيئة. والحل لمشكلة التحميل لا يكون غالباً بتغيير الإطار، وإنما برسم الصفحة في السيرفر وإرسال HTML جاهز، وهذا موضوع المقال القادم في هذه المرحلة عن الفرق بين CSR وSSR وSSG.

## كيف تختار؟ نقاط القوة ونقاط الضعف

من المهم أن تتذكر أن التجربة قاست تطبيقاً صغيراً، والقرار في المشاريع الحقيقية يتأثر بأشياء لا تقيسها التجربة: خبرة الفريق، والمكتبات المتاحة، وسهولة التوظيف، والأدوات المحيطة. وبحسب استطلاع [State of JavaScript 2024](https://2024.stateofjs.com/en-US/libraries/front-end-frameworks/?ref=arabroot.io) ما زال React الأكثر استخداماً بفارق كبير، وحافظ Vue على المرتبة الثانية، بينما تتصدر Svelte الترتيب في الآراء الإيجابية لمن يعرفونها.

**React، نقاط القوة:** أكبر مجتمع وأكبر عدد من المكتبات الجاهزة لكل شيء، من الجداول إلى النماذج إلى الرسوم البيانية، وأسهل إطار في التوظيف، ونموذج ذهني بسيط: دالة تأخذ حالة وترجع واجهة. وReact Native يسمح بنقل نفس المعرفة إلى تطبيقات الجوال. **نقاط الضعف:** أكبر حزمة في التجربة بأربع مرات من Svelte، وإعادة الرسم تشمل الشجرة كلها افتراضياً، فالأداء يحتاج انتباهاً لشكل البيانات وأماكن الحالة، والمترجم يساعد ولكنه لا يصلح التصميم كما رأيت.

**Vue، نقاط القوة:** التفاعلية الدقيقة تعطيك تحديثاً على مستوى المكون دون أن تكتب `memo`، والقوالب قريبة من HTML فيسهل على المطور القادم من خلفية الخادم أو التصميم أن يبدأ، و[التوثيق الرسمي](https://vuejs.org/guide/introduction.html?ref=arabroot.io) متكامل، والأدوات الأساسية مثل التوجيه وPinia رسمية من نفس الفريق، فلا تحتار في الاختيار. **نقاط الضعف:** مجتمع أصغر من React، وبعض أنماط التفاعلية مثل تفكيك الكائن التفاعلي تقطع التتبع بصمت، ووجود طريقتين لكتابة المكونات، Options API وComposition API، يربك الفرق الجديدة.

**Svelte، نقاط القوة:** أصغر حزمة، وتحديث دقيق على مستوى العقدة الواحدة، وكود أقصر في أغلب الحالات، ومع Runes صار نموذج التفاعلية واضحاً ومتسقاً داخل المكونات وخارجها. **نقاط الضعف:** أصغر مجتمع وعدد مكتبات أقل، والانتقال من Svelte 4 إلى 5 غير طريقة كتابة التفاعلية، فالكثير من الأمثلة على الإنترنت قديمة، وتعتمد كلياً على المترجم وخطوة البناء.

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

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

- **رفع كل الحالة إلى أعلى مكون.** نص حقل البحث في `App` يعني أن كل حرف يعيد تشغيل التطبيق كله في React. ضع الحالة في أقرب مكان لمن يستخدمها، ولا ترفعها إلا عند الحاجة.
- **إرسال كائن كامل إلى مكونات تحتاج جزءاً منه.** هذا ما جعل الألف صف تعمل في React حتى مع المترجم. أرسل لكل مكون القيمة التي يعرضها فقط.
- **استخدام ترتيب العنصر كمفتاح.** `key={index}` يجعل الإطار يظن أن الصف الأول بعد البحث هو نفس الصف الأول قبله، فتنتقل حالة الصفوف، مثل حقل مفتوح أو مربع مختار، إلى منتجات أخرى. استخدم معرفاً ثابتاً من البيانات.
- **حساب قيمة مشتقة وتخزينها في حالة منفصلة.** مجموع السلة ليس حالة، وإنما قيمة تحسب من السلة. خزنه في حالة ثانية وسوف يختلف عن السلة يوماً ما، وهذا ما يشرحه دليل React [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect?ref=arabroot.io). استخدم الحساب المباشر أو `computed` في Vue أو `$derived` في Svelte.
- **تفكيك كائن تفاعلي في Vue.** `const { count } = reactive(state)` يأخذ القيمة الحالية فقط ويفقد التتبع. استخدم `toRefs` أو اقرأ من الكائن مباشرة.
- **قياس الأداء على جهاز المطور فقط.** في تجربتنا اختفت الفروق كلها بدون تبطيء. فعل تبطيء المعالج في DevTools، واختبر على جوال متوسط.
- **عرض آلاف العناصر دفعة واحدة.** لا يوجد إطار يجعل إنشاء ألف صف سريعاً على جوال بطيء. قسم إلى صفحات أو استخدم العرض الافتراضي.
- **مخزن عام يكتب فيه كل مكون مباشرة.** اجعل التغيير عبر دوال محددة، حتى تعرف من غير ماذا عندما يظهر خطأ.

## جدول قرار Decision Table

الجدول التالي يجمع ما رأيناه في التجربة وما يتكرر في المشاريع الحقيقية:

| وضعك                                                                 | الاختيار الأنسب                         | لماذا                                                                   |
| -------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
| فريق كبير، أو توظيف مستمر، أو تحتاج مكتبات جاهزة كثيرة               | React مع React Compiler                 | أكبر مجتمع وأكبر سوق للمطورين، والمترجم يقلل العمل الزائد دون memo يدوي |
| فريق صغير يريد أداء جيداً دون ضبط يدوي، وأدوات رسمية متكاملة         | Vue                                     | تحديث على مستوى المكون افتراضياً، والتوجيه وPinia من نفس الفريق         |
| حجم JavaScript هو الأولوية، مثل جمهور على شبكات بطيئة أو أجهزة ضعيفة | Svelte                                  | ربع حجم React في التجربة، وتحديث على مستوى العقدة                       |
| تطبيق جوال أيضاً بنفس الفريق                                         | React                                   | React Native يشارك المفاهيم والمكتبات                                   |
| صفحات محتوى مع تفاعل قليل                                            | HTML من السيرفر مع JavaScript قليلة     | لا توجد حالة مشتركة كبيرة تحتاج مزامنة                                  |
| قائمة بآلاف العناصر                                                  | أي إطار مع Pagination أو Virtualization | تكلفة إنشاء الـ DOM أكبر من أي فرق بين الأطر                            |

وقبل أن تطلق تطبيقاً مبنياً بأي إطار راجع النقاط التالية:

- هل كل حالة في أقرب مكون يحتاجها، وهل المخزن المشترك فيه فقط ما تشترك فيه الصفحات فعلاً؟
- هل كل قائمة لها مفتاح ثابت من البيانات؟
- هل قست التفاعلات المهمة بتبطيء المعالج، ونظرت إلى عدد المكونات التي تعمل مع كل نقرة في أدوات الإطار مثل React DevTools Profiler أو Vue DevTools؟
- هل تعرف حجم حزمتك بعد الضغط، وما الذي يأخذ أكبر جزء منها؟
- هل تعرض في الصفحة الأولى عناصر أكثر مما يراه المستخدم على الشاشة؟

## تدريب عملي

- في نسخة React المباشرة غير `ProductList` لترسل لكل صف `qty={cart[p.id] || 0}` بدلاً من السلة كاملة، وابنها بالمترجم فقط دون `memo`، ثم شغل القياس، وقارن عدد الصفوف التي تعمل مع كل نقرة.
- انقل نص البحث في نسخة React إلى مكون بحث مستقل يرسل النتيجة إلى الأب فقط بعد توقف الكتابة 200 ms، وقس أثر ذلك على الكتابة.
- أضف إلى نسخة Svelte عرضاً افتراضياً بسيطاً يعرض 30 صفاً فقط حسب موضع التمرير، وقس زمن المسح من جديد.
- غير المفتاح في نسخة Vue إلى ترتيب العنصر، ثم اكتب في البحث وراقب عدد التعديلات على الـ DOM.

## الخلاصة

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

- الأطر الثلاثة تحل نفس المشكلة، وهي مزامنة الواجهة مع الحالة، ولكن React يعيد التشغيل ويقارن، وVue يتتبع من قرأ ماذا بالـ Proxy، وSvelte تترجم القالب إلى كود يحدث العقدة الواحدة.
- لنقرة واحدة أعاد React المباشر تشغيل ألف صف، وmemo صفاً واحداً، وVue صفاً واحداً مع الرأس، وSvelte لا شيء، مع أن تعديلات الـ DOM كانت 2 إلى 4 في كل الأطر.
- React Compiler لم يمنع إعادة الألف صف لأن كائن السلة نفسه تغير، فشكل البيانات الذي ترسله إلى المكونات أهم من الأداة.
- حجم الحزمة بعد gzip: 68.5 KB لـ React، و26.4 KB لـ Vue، و16.5 KB لـ Svelte، والمترجم أضاف 1.3 KB فقط قبل الضغط.
- على جهاز سريع اختفت فروق الزمن تقريباً، وعند إنشاء 875 صفاً كانت تكلفة الـ DOM هي الحاكمة في كل الأطر، فالحل هناك عرض عناصر أقل.
- ضع الحالة في أقرب مكان لمن يستخدمها، وأرسل لكل مكون ما يعرضه فقط، واستخدم مفاتيح ثابتة، وقس بتبطيء المعالج وأكثر من مرة.

وفي مرحلة [هندسة الواجهات وتطبيقات الويب](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/typescript-for-devs-cover-cover.webp)دليلTypeScript للمطورين: أي الأخطاء تكشفها الأنواع Types؟زرعنا تسعة أخطاء في كود سلة مشتريات وفحصناها بالمترجم tsc بثلاثة إعدادات، لنرى ما يكشفه الوضع الصارم وما يمر رغم كل الأنواع ولماذا تحتاج التحقق وقت التشغيل.](https://arabroot.io/articles/typescript-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D8%A7-%D8%A7%D9%84%D8%B0%D9%8A-%D8%AA%D9%83%D8%B4%D9%81%D9%87-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9/)[![](https://arabroot.io/content/images/2026/10/csr-ssr-ssg-cover-cover-1.webp)دليلCSR و SSR و SSG: أين ترسم صفحة الويب ومتى؟قسنا نفس صفحة المتجر مرسومة في المتصفح وفي السيرفر ومولدة مسبقاً على شبكة بطيئة في Chrome، لترى متى يظهر المحتوى ومتى تعمل الأزرار وماذا يرى محرك البحث.](https://arabroot.io/articles/%D8%A7%D9%84%D9%81%D8%B1%D9%82-%D8%A8%D9%8A%D9%86-csr-%D9%88-ssr-%D9%88-ssg-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%83%D9%84%D8%A7-%D9%85%D9%86%D9%87%D8%A7/)[![](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/css-layout-rtl-cover-cover.webp)دليلCSS للعربية واتجاه RTL: الخصائص المنطقية Logical Propertiesكيف تكتب ملف CSS واحداً يعمل بالعربية والإنجليزية، ولماذا تخطئ margin-left في الصفحة العربية، مع قياسات حقيقية من Chrome.](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/)

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

- أكتوبر 2026: كتابة المقال، وبناء التطبيق بـ React 19.3.1 وReact Compiler 1.0.0 وVue 3.5.44 وSvelte 5.57.3 عبر Vite 8.3.5، والقياس في Chrome 156.0.8078.49 بدون واجهة.