اكتشف السحر وراء أداء رياكت. يشرح هذا الدليل الشامل خوارزمية التسوية، ومقارنة الـ DOM الافتراضي، واستراتيجيات التحسين الرئيسية.
سر نجاح رياكت: نظرة عميقة على خوارزمية التسوية ومقارنة الـ DOM الافتراضي
في عالم تطوير الويب الحديث، أثبتت رياكت نفسها كقوة مهيمنة لبناء واجهات مستخدم ديناميكية وتفاعلية. لا تنبع شعبيتها من معماريتها القائمة على المكونات فحسب، بل من أدائها المذهل أيضًا. ولكن ما الذي يجعل رياكت سريعة جدًا؟ الجواب ليس سحرًا؛ بل هو قطعة هندسية رائعة تُعرف باسم خوارزمية التسوية (Reconciliation algorithm).
بالنسبة للعديد من المطورين، تُعتبر طريقة عمل رياكت الداخلية صندوقًا أسود. نحن نكتب المكونات، وندير الحالة، ونشاهد واجهة المستخدم تتحدث بسلاسة. ومع ذلك، فإن فهم الآليات وراء هذه العملية السلسة، وخاصة الـ DOM الافتراضي وخوارزمية المقارنة الخاصة به، هو ما يميز مطور رياكت الجيد عن المطور العظيم. تمنحك هذه المعرفة العميقة القدرة على كتابة تطبيقات مُحسَّنة للغاية، وتصحيح اختناقات الأداء، وإتقان المكتبة حقًا.
سيزيل هذا الدليل الشامل الغموض عن عملية التصيير الأساسية في رياكت. سنستكشف لماذا يكون التلاعب المباشر بالـ DOM مكلفًا، وكيف يقدم الـ DOM الافتراضي حلاً أنيقًا، وكيف تقوم خوارزمية التسوية بتحديث واجهة المستخدم بكفاءة. سنتعمق أيضًا في التطور من خوارزمية التسوية المكدسية (Stack Reconciler) الأصلية إلى معمارية فايبر (Fiber Architecture) الحديثة، ونختتم باستراتيجيات قابلة للتنفيذ يمكنك تطبيقها اليوم لتحسين تطبيقاتك الخاصة.
المشكلة الأساسية: لماذا يعتبر التلاعب المباشر بالـ DOM غير فعال
لتقدير حل رياكت، يجب أولاً أن نفهم المشكلة التي يحلها. نموذج كائن المستند (DOM) هو واجهة برمجة تطبيقات للمتصفح لتمثيل مستندات HTML والتفاعل معها. وهو منظم على شكل شجرة من الكائنات، حيث تمثل كل عقدة جزءًا من المستند (مثل عنصر أو نص أو سمة).
عندما تريد تغيير ما يظهر على الشاشة، فإنك تتلاعب بشجرة الـ DOM هذه. على سبيل المثال، لإضافة عنصر قائمة جديد، تقوم بإنشاء عنصر `
- `. على الرغم من أن هذا يبدو مباشرًا، إلا أن عمليات الـ DOM مكلفة من الناحية الحسابية. وإليك السبب:
- التخطيط وإعادة التدفق (Layout and Reflow): كلما غيرت هندسة عنصر ما (مثل عرضه أو ارتفاعه أو موضعه)، يضطر المتصفح إلى إعادة حساب مواضع وأبعاد جميع العناصر المتأثرة. تسمى هذه العملية "إعادة التدفق" أو "التخطيط" ويمكن أن تتالى عبر المستند بأكمله، مستهلكة قدرًا كبيرًا من طاقة المعالجة.
- إعادة الطلاء (Repainting): بعد إعادة التدفق، يحتاج المتصفح إلى إعادة رسم وحدات البكسل على الشاشة للعناصر المحدثة. يُطلق على هذا "إعادة الطلاء" أو "التنقيط". قد يؤدي تغيير شيء بسيط مثل لون الخلفية إلى إعادة طلاء فقط، لكن تغيير التخطيط سيؤدي دائمًا إلى إعادة الطلاء.
- متزامن ومعطِّل (Synchronous and Blocking): عمليات الـ DOM متزامنة. عندما يقوم كود جافاسكريبت الخاص بك بتعديل الـ DOM، غالبًا ما يضطر المتصفح إلى إيقاف المهام الأخرى مؤقتًا، بما في ذلك الاستجابة لإدخال المستخدم، لإجراء إعادة التدفق وإعادة الطلاء، مما قد يؤدي إلى واجهة مستخدم بطيئة أو متجمدة.
- التصيير الأولي: عندما يتم تحميل تطبيقك لأول مرة، تقوم رياكت بإنشاء شجرة DOM افتراضية كاملة لواجهة المستخدم الخاصة بك وتستخدمها لإنشاء الـ DOM الحقيقي الأولي.
- تحديث الحالة: عندما تتغير حالة التطبيق (على سبيل المثال، ينقر المستخدم على زر)، تقوم رياكت بإنشاء شجرة DOM افتراضية جديدة تعكس الحالة الجديدة.
- المقارنة (Diffing): تمتلك رياكت الآن شجرتي DOM افتراضيتين في الذاكرة: القديمة (قبل تغيير الحالة) والجديدة. ثم تقوم بتشغيل خوارزمية "المقارنة" الخاصة بها لمقارنة هاتين الشجرتين وتحديد الاختلافات الدقيقة.
- التجميع والتحديث: تحسب رياكت المجموعة الأكثر كفاءة وأقل عددًا من العمليات المطلوبة لتحديث الـ DOM الحقيقي ليتطابق مع الـ DOM الافتراضي الجديد. يتم تجميع هذه العمليات معًا وتطبيقها على الـ DOM الحقيقي في تسلسل واحد مُحسَّن.
- تقوم بتفكيك الشجرة القديمة بأكملها، وإلغاء تحميل جميع المكونات القديمة وتدمير حالتها.
- تقوم ببناء شجرة جديدة تمامًا من الصفر بناءً على نوع العنصر الجديد.
- العنصر B
- العنصر C
- العنصر A
- العنصر B
- العنصر C
- تقارن العنصر القديم في الفهرس 0 ('العنصر B') بالعنصر الجديد في الفهرس 0 ('العنصر A'). هما مختلفان، لذا تقوم بتعديل العنصر الأول.
- تقارن العنصر القديم في الفهرس 1 ('العنصر C') بالعنصر الجديد في الفهرس 1 ('العنصر B'). هما مختلفان، لذا تقوم بتعديل العنصر الثاني.
- ترى أن هناك عنصرًا جديدًا في الفهرس 2 ('العنصر C') وتقوم بإدراجه.
- العنصر B
- العنصر C
- العنصر A
- العنصر B
- العنصر C
- تنظر رياكت إلى أبناء القائمة الجديدة وتجد عناصر بمفاتيح 'b' و 'c'.
- تعرف أن العناصر ذات المفاتيح 'b' و 'c' موجودة بالفعل في القائمة القديمة، لذا تقوم ببساطة بنقلها.
- ترى أن هناك عنصرًا جديدًا بمفتاح 'a' لم يكن موجودًا من قبل، لذا تقوم بإنشائه وإدراجه.
- ... )`) هو نمط مضاد إذا كانت القائمة يمكن إعادة ترتيبها أو تصفيتها أو إضافة/إزالة عناصر من وسطها، لأنه يؤدي إلى نفس مشاكل عدم وجود مفتاح على الإطلاق. أفضل المفاتيح هي المعرفات الفريدة من بياناتك، مثل معرف قاعدة البيانات.
- التصيير التزايدي: يمكنها تقسيم عمل التصيير إلى أجزاء صغيرة وتوزيعه على إطارات متعددة.
- تحديد الأولويات: يمكنها تعيين مستويات أولوية مختلفة لأنواع مختلفة من التحديثات. على سبيل المثال، كتابة المستخدم في حقل إدخال لها أولوية أعلى من البيانات التي يتم جلبها في الخلفية.
- قابلية الإيقاف المؤقت والإلغاء: يمكنها إيقاف العمل مؤقتًا على تحديث منخفض الأولوية للتعامل مع تحديث عالي الأولوية، ويمكنها حتى إلغاء أو إعادة استخدام العمل الذي لم تعد هناك حاجة إليه.
- مرحلة التصيير/التسوية (غير متزامنة): في هذه المرحلة، تعالج رياكت عُقد فايبر لبناء شجرة "عمل قيد التقدم". تستدعي دوال `render` للمكونات وتشغل خوارزمية المقارنة لتحديد التغييرات التي يجب إجراؤها على الـ DOM. الأهم من ذلك، أن هذه المرحلة قابلة للمقاطعة. يمكن لرياكت إيقاف هذا العمل مؤقتًا للتعامل مع شيء أكثر أهمية، واستئنافه لاحقًا. ولأنها يمكن مقاطعتها، لا تطبق رياكت أي تغييرات فعلية على الـ DOM خلال هذه المرحلة لتجنب حالة واجهة مستخدم غير متسقة.
- مرحلة التثبيت (متزامنة): بمجرد اكتمال شجرة العمل قيد التقدم، تدخل رياكت مرحلة التثبيت. تأخذ التغييرات المحسوبة وتطبقها على الـ DOM الحقيقي. هذه المرحلة متزامنة ولا يمكن مقاطعتها. هذا يضمن أن المستخدم يرى دائمًا واجهة مستخدم متسقة. يتم تنفيذ دوال دورة الحياة مثل `componentDidMount` و `componentDidUpdate`، بالإضافة إلى خطافات `useLayoutEffect` و `useEffect`، خلال هذه المرحلة.
- `React.memo()`: مكون عالي الرتبة للمكونات الوظيفية. يقوم بإجراء مقارنة سطحية لخصائص المكون. إذا لم تتغير الخصائص، ستتخطى رياكت إعادة تصيير المكون وتعيد استخدام آخر نتيجة تم تصييرها.
- `useCallback()`: يتم إعادة إنشاء الدوال المعرفة داخل المكون في كل عملية تصيير. إذا قمت بتمرير هذه الدوال كخصائص لمكون ابن مغلف بـ `React.memo`، فسيتم إعادة تصيير المكون الابن لأن خاصية الدالة هي تقنيًا دالة جديدة في كل مرة. يقوم `useCallback` بحفظ الدالة نفسها في الذاكرة (memoizes)، مما يضمن إعادة إنشائها فقط إذا تغيرت تبعياتها.
- `useMemo()`: مشابه لـ `useCallback`، ولكن للقيم. يقوم بحفظ نتيجة عملية حسابية مكلفة في الذاكرة. يتم إعادة تشغيل الحساب فقط إذا تغيرت إحدى تبعياته. هذا مفيد لمنع العمليات الحسابية المكلفة في كل عملية تصيير وللحفاظ على استقرار مراجع الكائنات/المصفوفات التي يتم تمريرها كخصائص.
تخيل تطبيقًا معقدًا به آلاف العقد. إذا قمت بتحديث الحالة وأعدت تصيير واجهة المستخدم بأكملها بسذاجة عن طريق التلاعب المباشر بالـ DOM، فستجبر المتصفح على الدخول في سلسلة من عمليات إعادة التدفق وإعادة الطلاء المكلفة، مما يؤدي إلى تجربة مستخدم سيئة.
الحل: الـ DOM الافتراضي (VDOM)
أدرك مبتكرو رياكت عنق الزجاجة في أداء التلاعب المباشر بالـ DOM. كان حلهم هو تقديم طبقة تجريدية: الـ DOM الافتراضي.
ما هو الـ DOM الافتراضي؟
الـ DOM الافتراضي هو تمثيل خفيف الوزن في الذاكرة للـ DOM الحقيقي. إنه في الأساس كائن جافاسكريبت عادي يصف واجهة المستخدم. يحتوي كائن VDOM على خصائص تعكس سمات عنصر DOM حقيقي. على سبيل المثال، يمكن تمثيل `
{ type: 'div', props: { className: 'container', children: 'Hello World' } }
لأن هذه مجرد كائنات جافاسكريبت، فإن إنشائها والتلاعب بها سريع للغاية. لا يتضمن أي تفاعل مع واجهات برمجة تطبيقات المتصفح، لذلك لا توجد عمليات إعادة تدفق أو إعادة طلاء.
كيف يعمل الـ DOM الافتراضي؟
يمكّن الـ VDOM من اتباع نهج تعبيري لتطوير واجهة المستخدم. بدلاً من إخبار المتصفح كيفية تغيير الـ DOM خطوة بخطوة (بشكل أمري)، فإنك ببساطة تعلن عن الشكل الذي يجب أن تبدو عليه واجهة المستخدم لحالة معينة (بشكل تعبيري). تتولى رياكت الباقي.
تبدو العملية كالتالي:
من خلال تجميع التحديثات، تقلل رياكت من التفاعل المباشر مع الـ DOM البطيء، مما يحسن الأداء بشكل كبير. يكمن جوهر هذه الكفاءة في خطوة "المقارنة"، والتي تُعرف رسميًا باسم خوارزمية التسوية.
قلب رياكت: خوارزمية التسوية
التسوية هي العملية التي من خلالها تقوم رياكت بتحديث الـ DOM ليتطابق مع أحدث شجرة مكونات. الخوارزمية التي تقوم بهذه المقارنة هي ما نسميه "خوارزمية المقارنة".
نظريًا، يعد إيجاد الحد الأدنى من التحويلات لتحويل شجرة إلى أخرى مشكلة معقدة للغاية، مع تعقيد خوارزمي من رتبة O(n³)، حيث n هو عدد العقد في الشجرة. سيكون هذا بطيئًا جدًا للتطبيقات الواقعية. لحل هذه المشكلة، قام فريق رياكت ببعض الملاحظات الرائعة حول كيفية تصرف تطبيقات الويب عادةً ونفذ خوارزمية استدلالية أسرع بكثير - تعمل في زمن O(n).
الاستدلالات: جعل المقارنة سريعة وقابلة للتنبؤ
تعتمد خوارزمية المقارنة في رياكت على افتراضين أو استدلالين أساسيين:
الاستدلال الأول: أنواع العناصر المختلفة تنتج أشجارًا مختلفة
هذه هي القاعدة الأولى والأكثر مباشرة. عند مقارنة عقدتي VDOM، تنظر رياكت أولاً إلى نوعيهما. إذا كان نوع العناصر الجذرية مختلفًا، تفترض رياكت أن المطور لا يريد محاولة تحويل أحدهما إلى الآخر. بدلاً من ذلك، تتخذ نهجًا أكثر جذرية ولكنه قابل للتنبؤ:
على سبيل المثال، ضع في اعتبارك هذا التغيير:
قبل: <div><Counter /></div>
بعد: <span><Counter /></span>
على الرغم من أن المكون الابن `Counter` هو نفسه، ترى رياكت أن الجذر قد تغير من `div` إلى `span`. ستقوم بإلغاء تحميل `div` القديم ومثيل `Counter` بداخله بالكامل (مما يفقد حالته) ثم تقوم بتحميل `span` جديد ومثيل جديد تمامًا من `Counter`.
نقطة رئيسية: تجنب تغيير نوع العنصر الجذر لشجرة مكون فرعية إذا كنت ترغب في الحفاظ على حالته أو تجنب إعادة تصيير كاملة لتلك الشجرة الفرعية.
الاستدلال الثاني: يمكن للمطورين الإشارة إلى العناصر المستقرة باستخدام خاصية `key`
يمكن القول إن هذا هو الاستدلال الأكثر أهمية الذي يجب على المطورين فهمه وتطبيقه بشكل صحيح. عندما تقارن رياكت قائمة من العناصر الأبناء، فإن سلوكها الافتراضي هو التكرار على كلتا قائمتي الأبناء في نفس الوقت وإنشاء طفرة حيثما يوجد اختلاف.
مشكلة المقارنة المستندة إلى الفهرس
لنتخيل أن لدينا قائمة من العناصر ونضيف عنصرًا جديدًا إلى بداية القائمة دون استخدام مفاتيح.
القائمة الأولية:
القائمة المحدثة (إضافة 'العنصر A' في البداية):
بدون مفاتيح، تقوم رياكت بإجراء مقارنة بسيطة تعتمد على الفهرس:
هذا غير فعال للغاية. لقد أجرت رياكت تعديلين غير ضروريين وإدراجًا واحدًا، في حين أن كل ما كان مطلوبًا هو إدراج واحد في البداية. إذا كانت عناصر القائمة هذه مكونات معقدة لها حالتها الخاصة، فقد يؤدي ذلك إلى مشاكل خطيرة في الأداء وأخطاء، حيث يمكن أن تختلط الحالة بين المكونات.
قوة خاصية `key`
توفر خاصية `key` حلاً. إنها سمة سلسلة خاصة تحتاج إلى تضمينها عند إنشاء قوائم من العناصر. تمنح المفاتيح رياكت هوية مستقرة لكل عنصر.
دعنا نعود إلى نفس المثال، ولكن هذه المرة بمفاتيح مستقرة وفريدة:
القائمة الأولية:
القائمة المحدثة:
الآن، عملية المقارنة في رياكت أذكى بكثير:
هذا أكثر كفاءة بكثير. تحدد رياكت بشكل صحيح أنها تحتاج فقط إلى إجراء إدراج واحد. يتم الحفاظ على المكونات المرتبطة بالمفاتيح 'b' و 'c'، مع الحفاظ على حالتها الداخلية.
قاعدة حاسمة للمفاتيح: يجب أن تكون المفاتيح مستقرة وقابلة للتنبؤ وفريدة بين أشقائها. استخدام فهرس المصفوفة كمفتاح (`items.map((item, index) =>
التطور: من معمارية المكدس إلى معمارية فايبر
كانت خوارزمية التسوية الموصوفة أعلاه أساس رياكت لسنوات عديدة. ومع ذلك، كان لها قيد رئيسي واحد: كانت متزامنة ومعطِّلة. يشار الآن إلى هذا التنفيذ الأصلي باسم مسوّي المكدس (Stack Reconciler).
الطريقة القديمة: مسوّي المكدس (Stack Reconciler)
في مسوّي المكدس، عندما يؤدي تحديث الحالة إلى إعادة تصيير، كانت رياكت تجتاز بشكل تعاودي شجرة المكونات بأكملها، وتحسب التغييرات، وتطبقها على الـ DOM - كل ذلك في تسلسل واحد غير منقطع. بالنسبة للتحديثات الصغيرة، كان هذا جيدًا. ولكن بالنسبة لأشجار المكونات الكبيرة، قد تستغرق هذه العملية وقتًا طويلاً (على سبيل المثال، أكثر من 16 مللي ثانية)، مما يعطل الخيط الرئيسي للمتصفح. كان هذا يتسبب في أن تصبح واجهة المستخدم غير مستجيبة، مما يؤدي إلى إسقاط الإطارات، ورسوم متحركة متقطعة، وتجربة مستخدم سيئة.
تقديم رياكت فايبر (React 16+)
لحل هذه المشكلة، قام فريق رياكت بمشروع استمر لعدة سنوات لإعادة كتابة خوارزمية التسوية الأساسية بالكامل. النتيجة، التي تم إصدارها في React 16، تسمى رياكت فايبر (React Fiber).
تم تصميم معمارية فايبر من الألف إلى الياء لتمكين التزامن - قدرة رياكت على العمل على مهام متعددة في وقت واحد والتبديل بينها بناءً على الأولوية.
"الفايبر" هو كائن جافاسكريبت عادي يمثل وحدة عمل. يحتوي على معلومات حول المكون، ومدخلاته (props)، ومخرجاته (children). بدلاً من الاجتياز التعاودي الذي لا يمكن مقاطعته، تقوم رياكت الآن بمعالجة قائمة مرتبطة من عقد فايبر، واحدة تلو الأخرى.
فتحت هذه المعمارية الجديدة العديد من القدرات الرئيسية:
المرحلتان في فايبر
في ظل فايبر، تنقسم عملية التصيير إلى مرحلتين متميزتين:
تُعد معمارية فايبر الأساس للعديد من ميزات رياكت الحديثة، بما في ذلك `Suspense`، والتصيير المتزامن، و`useTransition`، و`useDeferredValue`، وكلها تساعد المطورين على بناء واجهات مستخدم أكثر استجابة وسلاسة.
استراتيجيات تحسين عملية للمطورين
إن فهم عملية التسوية في رياكت يمنحك القدرة على كتابة كود أكثر أداءً. فيما يلي بعض الاستراتيجيات القابلة للتنفيذ:
1. استخدم دائمًا مفاتيح مستقرة وفريدة للقوائم
لا يمكن التأكيد على هذا بما فيه الكفاية. إنه أهم تحسين فردي للقوائم. استخدم معرفًا فريدًا من بياناتك (على سبيل المثال، `product.id`). تجنب استخدام فهارس المصفوفة إلا إذا كانت القائمة ثابتة تمامًا ولن تتغير أبدًا.
2. تجنب عمليات إعادة التصيير غير الضرورية
يتم إعادة تصيير المكون إذا تغيرت حالته أو تمت إعادة تصيير المكون الأصل. في بعض الأحيان، يتم إعادة تصيير المكون حتى لو كان ناتجه سيكون مطابقًا. يمكنك منع هذا باستخدام:
3. تكوين المكونات بذكاء
الطريقة التي تهيكل بها مكوناتك يمكن أن يكون لها تأثير كبير على الأداء. إذا كان جزء من حالة مكونك يتحدث بشكل متكرر، حاول عزله عن الأجزاء التي لا تتحدث.
على سبيل المثال، بدلاً من وجود مكون واحد كبير حيث يتسبب حقل إدخال يتغير بشكل متكرر في إعادة تصيير المكون بأكمله، ارفع تلك الحالة إلى مكون أصغر خاص بها. بهذه الطريقة، يتم إعادة تصيير المكون الصغير فقط عندما يكتب المستخدم.
4. استخدام المحاكاة الافتراضية (Virtualization) للقوائم الطويلة
إذا كنت بحاجة إلى تصيير قوائم بمئات أو آلاف العناصر، حتى مع وجود مفاتيح مناسبة، فإن تصييرها جميعًا مرة واحدة يمكن أن يكون بطيئًا ويستهلك الكثير من الذاكرة. الحل هو المحاكاة الافتراضية أو التقسيم إلى نوافذ (windowing). تتضمن هذه التقنية تصيير المجموعة الصغيرة فقط من العناصر المرئية حاليًا في منفذ العرض. أثناء تمرير المستخدم، يتم إلغاء تحميل العناصر القديمة، وتحميل العناصر الجديدة. توفر مكتبات مثل `react-window` و `react-virtualized` مكونات قوية وسهلة الاستخدام لتنفيذ هذا النمط.
الخاتمة
أداء رياكت ليس صدفة؛ إنه نتيجة لمعمارية مدروسة ومتطورة تتمحور حول الـ DOM الافتراضي وخوارزمية تسوية فعالة. من خلال تجريد التلاعب المباشر بالـ DOM، يمكن لرياكت تجميع التحديثات وتحسينها بطريقة سيكون من المعقد جدًا إدارتها يدويًا.
كمطورين، نحن جزء حاسم من هذه العملية. من خلال فهم استدلالات خوارزمية المقارنة - استخدام المفاتيح بشكل صحيح، وحفظ المكونات والقيم في الذاكرة، وهيكلة تطبيقاتنا بعناية - يمكننا العمل مع مسوّي رياكت، وليس ضده. لقد دفع التطور إلى معمارية فايبر حدود ما هو ممكن، مما مكن جيلاً جديدًا من واجهات المستخدم السلسة والمستجيبة.
في المرة القادمة التي ترى فيها واجهة المستخدم الخاصة بك تتحدث على الفور بعد تغيير الحالة، خذ لحظة لتقدير الرقص الأنيق للـ DOM الافتراضي، وخوارزمية المقارنة، ومرحلة التثبيت التي تحدث تحت الغطاء. هذا الفهم هو مفتاحك لبناء تطبيقات رياكت أسرع وأكثر كفاءة وقوة لجمهور عالمي.