تحسين الأداء في React Native

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

أحد الأسباب المقنعة لاستخدام React Native بدلاً من أدواتٍ تعتمد على عارض الويب WebView هو الحصول على تطبيق يعمل بمعدّل 60 إطارًا في الثانية مع شكل ومظهر أصيلٍ. يعمل مطورو React Native على تطوير الإطار ليُنجز المهام بشكل صحيح وفعّال، ليساعدك ذلك على التركيز على تطبيقك بدلاً من تحسين الأداء، ولكن هناك نقصٌ في بعض المناطق، وفي مناطق أخرى لا يُمكن فيها لإطار العمل (كما هو الحال مع كتابة شيفرة باللغة الأصيلة) أن يُحدّد أفضل طريقة للتحسين من أجلك، وبالتالي سيكون التدخل اليدوي ضروريًا. يبذل مطورو إطار العمل قصارى جهدهم لتقديم أداء واجهة مستخدم سلسَةٍ وسريعة افتراضيًّا، لكن أحيانًا لا يكون هذا ممكنًا، لذا عليك أن تُحسّن أداء تطبيقك بنفسك في هذه الحالات.

يهدف هذا الدليل إلى تعليمك بعض الأساسيات لمساعدتك على استكشاف مشاكل الأداء وإصلاحها، وسنناقش كذلك المصادر الشائعة للمشاكل والحلول المقترحة لها.

ما تحتاج إلى معرفته حول الإطارات (frames)

وُصِفَت الأفلام بعبارة "الصور المتحركة" لسببٍ واضح: فالحركة الواقعية في مقاطع الفيديو ما هي إلّا وهم يخلقه تغييرٌ سريعٌ للصور الثابتة بسرعة ثابتة. نشير إلى كل صورة من هذه الصور على أنّها إطارٌ واحد. لعددِ الإطارات الذي يُعرَض في كل ثانية تأثيرٌ مباشر على كيفية ظهور الفيديو (أو واجهة المستخدم) من ناحية السلاسة والواقعيّة. تعرض أجهزة iOS واجهة المستخدم بمعدّل60 إطارًا في الثانية، ما يمنحك ونظامَ واجهة المستخدم حوالي 16.67 مللي ثانية للقيام بكل العمل المطلوب لتوليد الصورة الثابتة (الإطار) التي سيراها المستخدم على الشاشة لتلك الفترة الزمنية. إذا لم تقدِر على القيام بالعمل الضروري لتوليد هذا الإطار ضمن الفترة الزمنيّة المخصّصة (16.67ms)، فسوف "تُسقِط إطارًا" وستظهر واجهة المستخدم وكأنّها غير مستجيبة.

لمزيدٍ من الإرباك، افتح قائمة المطورين (developer menu) في تطبيقك وقم بتمكين Show Perf Monitor. ستلاحظ أن هناك معدلي أطرٍ (frame rates) مختلفين.

PerfUtil.png

معدل إطارات JS (سلسلة JavaScript)

في معظم تطبيقات React Native، سيُنفَّذ منطق معاملاتك (business logic) على السلسلة (thread) الخاصّة بلغة JavaScript. هذه هي المنطقة التي يوجد فيها تطبيق React الخاص بك، وهنا تُجرى استدعاءات الواجهات البرمجيّة ومعالجة الأحداث، وما إلى ذلك. تُجمَّع التحديثات التي تتعلّق بالواجهات الأصيلة (native-backed views) وتُرسَل إلى الجانب الأصيل في نهاية كل تكرار من تكرارات حلقة الأحداث (event loop) قبل الموعد النهائي للإطار (هذا إذا سارت الأمور على ما يرام). إذا لم تستجب سلسلة JavaScript لإطارٍ ما، فسَيُعَدُّ إطارًا منسدلًا (dropped frame). على سبيل المثال، إذا استدعيت الدالة this.setState على المكون الجذر لتطبيقٍ معقّد، وأدّى ذلك إلى إعادة تصيير مكوّناتٍ فرعيةٍ تأخذ الكثير من الوقت بسبب إجراء حسابات معقّدة، فمن المرجّح أن هذا قد يستغرق 200 مللي ثانية ويؤدي إلى إسقاط 12 إطارًا. ستظهر أي تحريكاتٍ مُتحّكَمٍ فيها من طرف JavaScript مُجمَّدَةً خلال تلك الفترة. إذا استغرق أي شيء أكثر من 100 مللي ثانية، فسيشعر المستخدم بذلك.

يحدث هذا غالبًا أثناء انتقالات Navigator: عندما تدفعُ موجّهًا (push a route) جديدًا، ستحتاج سلسلة JavaScript إلى تصيير جميع المكونات اللازمة للمشهد لإرسال الأوامر الصحيحة إلى الجانب الأصيل لإنشاء الواجهات الجديدة. من الشائع أن تأخذ هذه العمليّة بعض الإطارات ومن الممكن أن يُسبب ذلك في حدوث ظاهرة jank (أي ظهور التطبيق وكأنه لا يستجيب لتفاعل المستخدم معه) لأن الانتقال مُتحّكَمٌ فيه من طرف سلسلة JavaScript. في بعض الأحيان، تؤدّي المكونات عملًا إضافيًّا في التابع componentDidMount، مما قد يؤدي إلى تأخّرٍ (أو تأتأةٍ: stutter) ثانٍ في الانتقال.

الاستجابة للمسات مثالٌ آخر: إذا كنت تقوم بعملياتٍ عبر عدّة إطارات في سلسلة JavaScript، فقد تلاحظ وجود تأخيرٍ في الاستجابة إلى المكوّن TouchableOpacity مثلًا. ويرجع ذلك إلى أن سلسلة JavaScript مشغولة ولا يمكنها معالجة أحداث اللمس الأولية المرسلة من السلسلة الرئيسيّة. والنتيجة أنّ المكوّن TouchableOpacity غير قادرٍ على التّفاعل مع أحداث اللمس لإخبار الواجهة الأصيلة بتغيير عتامتها (opacity).

معدل إطارات واجهة المستخدم (السلسلة الرئيسية)

لاحظ العديد من الأشخاص أن أداء المكوّن ‎NavigatorIOS‎ أفضلُ من المكوّن ‎Navigator‎ (الذي أُزيل من إطار العمل منذ مُدّة). سبب هذا هو أنّ تحريكات الانتقال تتم كليًّا على السلسلة الرئيسية، ولذلك لا تُقاطَع من طرف الإطارات المنسدلة على سلسلة JavaScript.

وبالمثل، يمكنك التمرير لأعلى ولأسفل في واجهة ScrollView عندما تُقفَل سلسلة JavaScript لأن مكوّن ScrollView موجود في السلسلة الرئيسية. تُرسَل أحداث التمرير (scroll events) إلى سلسلة JavaScript، لكنّ استقبالها ليس ضروريًا ليحدث التمرير.

مصادر شائعة لمشاكل الأداء

تشغيل التطبيق في وضع التطوير (dev=true)

يتأثر أداء سلسلة JavaScript بشدّةٍ عند التشغيل في وضع التطوير. هذا أمرٌ لا مفر منه، إذ يجب القيام بالكثير من العمل في وقت التشغيل (runtime) لتزويدك بتحذيرات ورسائل خطأ جيدة، مثل التحقق من أنواع propTypes ومختلف التأكيدات (assertions) الأخرى. تأكد دائمًا من اختبار الأداء في بناءات الإصدار (release builds).

استخدام جمل console.log

عند تشغيل تطبيق مُجمّع (bundled app)، يمكن أن تتسبب جمل console.log في حدوث خنق كبير في سلسلة JavaScript. وهذا يشمل الاستدعاءات من مكتبات التنقيح مثل redux-logger، لذا تأكد من إزالتها قبل التحزيم. يمكنك كذلك استخدام إضافة babel هذه التي تزيل جميع استدعاءات ‎console.*‎. ستحتاج إلى تثبيتها أولاً باستخدام ‎npm i babel-plugin-transform-remove-console --save-dev‎، ثم عدِّل ملف ‎.babelrc‎ داخل مشروعك على النحو التالي:

{
  "env": {
    "production": {
      "plugins": ["transform-remove-console"]
    }
  }
}

سيؤدي هذا إلى إزالة كافة استدعاءات ‎console.*‎ في نسخ الإصدار (أو الإنتاج [production]) من مشروعك.

تصيير مكون ListView الأولي بطيء جدًا أو أن أداء تمرير القوائم الكبيرة سيئ

استخدم مكون FlatList أو SectionList الجديدين بدلًا من المكون القديم ListView الذي أصبح مُهملًا (deprecated). إلى جانب تبسيط واجهة برمجة التطبيقات (API)، تحتوي مكونات القوائم الجديدة كذلك على تحسيناتِ أداءٍ كبيرة، أكبر هذه التحسينات هو استخدام نفس حجم الذاكرة تقريبًا لأي عدد من الصفوف.

إذا كان تصيير المكوّن FlatList بطيئًا في تطبيقك، فاستعمل الخاصيّة getItemLayout لتحسين سرعة التصيير عبر تخطي قياس العناصر المصيَّرة.

ينخفض معدل إطارات JS في الثانيّة (JS FPS) عند إعادة تصيير مكون لا يتغير تقريبا

إذا كنت تستخدم مكوّن ListView، عليك توفير دالة rowHasChanged التي يمكن لها أن تختصر الكثير من الجهد بتحديد ما إذا كان الصف يحتاج إلى إعادة تصييره سريعًا. إذا كنت تستخدم هياكل بيانات غير قابلة للتغيير (immutable data structures)، فسيكون هذا بسيطًا عبر التحقق من المساواة المرجعية (reference equality).

وبالمثل، يمكنك استخدام shouldComponentUpdate والإشارة إلى الشروط الدقيقة التي ترغب بإعادة تصيير المكون بها. إذا كنت تكتب مكونات نقية (حيث تعتمد القيمة المعادة [return value] لدالة التصيير [render function] اعتمادًا كاملًا على الخاصيات والحالة)، يمكنك الاستفادة من المكون PureComponent الصرف للقيام بذلك نيابة عنك. مجدّدًا، هياكل البيانات غير القابلة للتغيير مفيدة للحفاظ على سرعة الأداء، إذا توجَّب عليك القيام بمقارنةٍ عميقةٍ (deep comparison) لقائمة كبيرة من الكائنات، فقد تكون إعادة تصيير المكون بأكمله أسرع، وسيتطلب ذلك شيفرةً أقل بالتأكيد.

سقوط إطارات على سلسلة JS بسبب إجراء عمليات كثيرة على سلسلة JS في نفس الوقت

التنقّل البطيء في المتنقّل (Navigator) من المظاهر الشائعة لهذه المشكلة، لكن هناك حالات أخرى يحدث فيها هذا. يمكن لاستخدام واجهة InteractionManager البرمجيّة أن يكون طريقة جيدة لحلّها، لكن إذا كان تأخير تحريكٍ مُكلّفًا على تجربة المستخدم. فانظر واجهة LayoutAnimation البرمجية.

تحسب واجهة Animated حاليًا كل إطارٍ مفتاحيّ (keyframe) حسب الطلب (on-demand) على سلسلة JavaScript، إلا إذا قمت بضبط useNativeDriver: true، في حين أن واجهة LayoutAnimation تعتمد على التحريكات الأساسية (Core Animation) ولا تتأثر بسقوط الإطارات في سلسلة JS والسلسلة الرئيسية.

هذه حالة يُمكن فيها استخدام هذه الطريقة: التحريك في مربّع حوار (التمرير لأسفل من أعلى مع التلاشي في تراكبٍ نصف شفاف) أثناء التهيئة وربما تلقّي استجابات لطلباتٍ متعددة من الشبكة، وتقديم محتويات النافذة، وكذا تحديث العرض الذي فُتحت منه النافذة. انظر دليل "التحريكات" لمزيد من المعلومات حول كيفية استخدام واجهة LayoutAnimation.

تحذير: لا يعمل LayoutAnimation إلا مع التحريكات الساكنة (‎"static"‎ animations‎)، أي تلك التي تبدأ وتستمر في العمل دون حاجة إلى تغييرها. إذا لزِمَت مقاطعة التحريكات، فستحتاج إلى استخدام واجهة ‎Animated‎.

نقل واجهة على الشاشة (التمرير، الترجمة، التدوير) يسقط معدل الإطارات على سلسلة واجهة المستخدم

يحدث هذا بشكل خاص عندما يكون لديك نص بخلفية شفّافة موضوعٍ فوق صورة، أو أي حالة أخرى تتطلب تركيب ألفا (alpha compositing) لإعادة رسم الواجهة على كل إطار. قد تجد أنّ تمكينَ ‎shouldRasterizeIOS‎ أو ‎renderToHardwareTextureAndroid‎ يساعدُ بشكل ملحوظ.

اِحرص على عدم الإفراط في استخدام هذه الحيلة، فقد تستنزف ذاكرة الهاتف. افحص حالة الأداءِ وحالة استخدامِ الذاكرة عند الاعتماد على هذه الخاصيات. إذا كنت تُخطّط لإيقاف تحريك الواجهة، فعطّل هذه الخاصية.

يسقط تحريك حجم صورة معدل الإطارات على سلسلة واجهة المستخدم

على iOS، في كل مرة تغيّر فيها ضبط عرض أو ارتفاع مكوّن Image، يُعاد اقتصاصها وتحجيمها من الصورة الأصلية. قد يكون هذا مكلفًا جدا، خاصّة للصور الكبيرة. بدلاً من هذا، اِستخدم خاصية النمط ‎transform: [{scale}]‎ لتحريك الحجم. النقر فوق صورة وتكبيرها إلى وضع ملء الشاشة من الأمثلة على حالة من حالات الاعتماد على هذه الخاصية.

واجهة ‎TouchableX‎ لا تستجيب بسرعة

أحيانًا، إذا أجرينا عمليّة في نفس الإطار الذي نُعدّل فيه العتامة (opacity) أو الإبراز (highlight) لأحد المكونات التي تستجيب إلى اللمس، فلن نرى هذا التأثير إلا بعد أن تُعيد الدالة onPress. إذا كانت الدالة onPress تغيّر الحالة بالدالة setState، ما يحتاج إلى عمليات مكلّفة ويسبّب سقوط بعض الإطارات، فقد يبدو المكون القابل للمس غير مستجيب. أحد حلول هذه المشكلة هو تغطية أي عمليات داخل معالج الدالة onPress بالدالة requestAnimationFrame كما يلي:

handleOnPress() {
  requestAnimationFrame(() => {
    this.doExpensiveAction();
  });
}

انتقالات بطيئة في مكون Navigator

كما ذُكِر أعلاه، يُتحكَّمُ في تحريكات Navigator بواسطة سلسلة JavaScript. تخيل مشهد الانتقال "من اليمين إلى اليسار": يُنقَل المشهد الجديد في كل إطار من اليمين إلى اليسار، بدءًا من خارج الشاشة (عند نقطة x-offset تساوي 320 مثلًا) إلى أن يستقّر في النهاية عند نقطة x-offset تساوي 1.

في كل إطار أثناء هذا النقل، ستحتاج سلسلة JavaScript إلى إرسال نقطة x-offset جديدة إلى السلسلة الرئيسية. إذا كانت سلسلة JavaScript مُقفلَةً (locked up)، فلا يمكن القيام بهذا، وبالتالي لا يحدث أي تحديث على ذلك الإطار وستتأخّر التحريكات.

أحد الحلول لهذه المشكلة هو السماح للتحريكات المعتمدة على JavaScript بنقل تحميلها (offloaded) إلى السلسلة الرئيسية. لو كتبنا نفس المثال أعلاه بهذا الأسلوب، فمن الممكن مثلا أن نحسب قائمة بجميع نقاط x-offsets للمشهد الجديد عندما نبدأ الانتقال، ثمّ نرسلها إلى السلسلة الرئيسية لتنفيذها بطريقة محسنة. الآن بعد تحرير سلسلة JavaScript من هذه المسؤولية، لن يكون سقوط بعض الإطارات أثناء تصيير المشهد مشكلة كبيرة. قد لا تلاحظ ذلك حتى، لأنّ الانتقال الجميل سيُشتّت انتباهك.

حل هذه المشكلة أحد الأهداف الرئيسية لمكتبة React Navigation الجديدة. تَستخدِم الواجهات في React Navigation مكوناتٍ أصيلة ومكتبة Animated لتقديم تحريكاتٍ بمعدّل 60 إطارًا في الثانية تُشغَّل على السلسلة الأصيلة.

مصادر