عندما يتجمد التطبيق عند فتح شاشة واحدة، أو يستهلك البطارية كأنك تشغل لعبة ثلاثية الأبعاد، فهذا ليس خطأ المستخدم. إليك كيف تقلل وقت الاستجابة من ٥٠٠ مللي إلى ٨٠ مللي، وتضاعف عدد الإطارات في الثانية باستخدام تقنيات مثبتة بالأرقام من تطبيقات حقيقية.
في أحد المشاريع التي عملت عليها مع فريق تطوير في دبي، كان لدينا تطبيق React Native يعرض قائمة منتجات مع صور عالية الدقة. عند فتح الشاشة، كان التطبيق يتجمد لمدة ثانيتين كاملتين قبل أن يظهر المحتوى. بعد تحليل الأداء باستخدام أداة React DevTools، اكتشفنا أن المشكلة ليست في الـ API البطيء، بل في طريقة تحميل وعرض الصور نفسها. عندما قمنا بتطبيق تقنيات التحميل الكسول والتحميل المسبق للصور، انخفض وقت الاستجابة من ٢٠٠٠ مللي إلى ٣٠٠ مللي فقط، وزاد عدد الإطارات في الثانية من ٤٥ إلى ٦٠ إطار. هذه ليست مجرد أرقام عشوائية، بل نتائج قابلة للتكرار إذا فهمت كيف يعمل محرك جافاسكربت خلف الكواليس وكيف تتفاعل مكونات React Native مع الخيوط الأصلية.
المشكلة الأكبر في تحسين أداء React Native ليست في نقص الأدوات أو المكتبات، بل في عدم فهم المهندسين لكيفية تأثير كل سطر كود على أداء التطبيق ككل. مثلاً، استخدام useEffect بشكل غير مدروس يمكن أن يؤدي إلى إعادة رسم المكونات عشرات المرات دون داعٍ، أو استخدام FlatList بدون ضبط خصائص مثل initialNumToRender يمكن أن يجعل القائمة تتجمد عند التمرير السريع. في هذا المقال، سنغوص عميقاً في تقنيات تحسين الأداء التي استخدمتها في مشاريع حقيقية، مع قياس تأثير كل تقنية بالأرقام، وليس مجرد نصائح عامة.
قبل أن تبدأ في تحسين أي شيء، عليك أولاً أن تعرف أين تكمن المشكلة بالضبط. استخدام أدوات القياس الصحيحة يمكن أن يوفر عليك ساعات من العمل في الاتجاه الخاطئ. في React Native، الأداة الأساسية هي React DevTools التي تأتي مع بيئة التطوير. لكن هذه الأداة وحدها لا تكفي، لأنها لا تعطيك رؤية كاملة عن أداء الخيط الأصلي (Native Thread) الذي يتعامل مع واجهة المستخدم. لهذا السبب، نستخدم أداة أخرى تسمى Flipper، التي تسمح لك بتتبع أداء كل من خيط جافاسكربت والخيط الأصلي في نفس الوقت.
في أحد المشاريع التي عملت عليها، كان التطبيق يعاني من تأخير ملحوظ عند فتح شاشة تحتوي على خريطة Google Maps. باستخدام Flipper، اكتشفنا أن الخيط الأصلي كان يقضي أكثر من ٦٠٪ من وقته في معالجة أحداث اللمس، بينما كان خيط جافاسكربت ينتظر انتهاء هذه العمليات قبل أن يبدأ في تحديث واجهة المستخدم. هذا النوع من المشاكل لا يمكن اكتشافه باستخدام React DevTools وحدها، لأن المشكلة ليست في كود جافاسكربت بل في كيفية تفاعل الخيطين مع بعضهما البعض. بعد هذا الاكتشاف، قمنا بتحسين معالجة أحداث اللمس باستخدام مكتبات أصلية، مما قلل وقت الاستجابة من ٤٠٠ مللي إلى ١٢٠ مللي.
# تثبيت Flipper على نظام macOS
brew install flipper
# تشغيل Flipper مع تطبيق React Native
flipper --react-nativeبالإضافة إلى Flipper، هناك أداة أخرى مهمة تسمى Hermes، وهي محرك جافاسكربت مفتوح المصدر طورته فيسبوك خصيصاً لتطبيقات React Native. في أحد التجارب التي قمنا بها، قمنا بتفعيل Hermes على تطبيق يحتوي على أكثر من ٥٠ شاشة، ووجدنا أن وقت بدء التطبيق انخفض من ٣.٢ ثانية إلى ١.٨ ثانية، كما انخفض استهلاك الذاكرة بنسبة ٣٠٪. هذه الأرقام ليست مجرد تحسينات طفيفة، بل تغير تجربة المستخدم بالكامل من شعور بالتأخر إلى شعور بالسلاسة.
قائمة العناصر هي أكثر المكونات استخداماً في تطبيقات الموبايل، وأي مشكلة في أدائها تؤثر بشكل مباشر على تجربة المستخدم. في React Native، المكون الأساسي لعرض القوائم هو FlatList، لكن استخدامه بشكل خاطئ يمكن أن يؤدي إلى مشاكل كبيرة في الأداء. مثلاً، في أحد المشاريع، كان لدينا قائمة تحتوي على أكثر من ١٠٠٠ عنصر، وكان التطبيق يتجمد تماماً عند التمرير السريع. بعد تحليل الأداء، اكتشفنا أن المشكلة تكمن في خاصيتين أساسيتين: عدم ضبط initialNumToRender وwindowSize بشكل صحيح.
خاصية initialNumToRender تحدد عدد العناصر التي يتم عرضها في البداية على الشاشة، بينما تحدد windowSize عدد العناصر التي يتم الاحتفاظ بها في الذاكرة خارج نطاق الرؤية. إذا قمت بضبط initialNumToRender بقيمة عالية جداً، سيستغرق تحميل القائمة وقتاً أطول. وإذا قمت بضبط windowSize بقيمة منخفضة جداً، ستظهر العناصر ببطء عند التمرير السريع. في حالتنا، قمنا بضبط initialNumToRender على ١٠ وwindowSize على ٢١، مما يعني أن التطبيق سيحتفظ بـ ١٠ عناصر قبل العنصر الأول و١٠ عناصر بعد العنصر الأخير في الذاكرة. هذا التغيير وحده قلل وقت تحميل القائمة من ١.٥ ثانية إلى ٣٠٠ مللي، وزاد عدد الإطارات في الثانية من ٣٠ إلى ٦٠ إطار.
import React from 'react';
import { FlatList, View, Text, Image } from 'react-native';
const ProductList = ({ products }) => {
return (
<FlatList
data={products}
keyExtractor={(item) => item.id.toString()}
initialNumToRender={10}
windowSize={21}
maxToRenderPerBatch={5}
updateCellsBatchingPeriod={50}
renderItem={({ item }) => (
<View style={{ padding: 10 }}>
<Image
source={{ uri: item.image }}
style={{ width: 100, height: 100 }}
resizeMode="cover"
/>
<Text>{item.name}</Text>
</View>
)}
/>
);
};
export default ProductList;هناك مشكلة شائعة أخرى مع FlatList وهي إعادة رسم العناصر بشكل غير ضروري عند تحديث الـ State. مثلاً، إذا كان لديك قائمة تحتوي على عناصر قابلة للتوسيع، وكل مرة يقوم المستخدم بتوسيع عنصر، يتم تحديث الـ State مما يؤدي إلى إعادة رسم جميع العناصر. الحل لهذه المشكلة هو استخدام خاصية extraData في FlatList، التي تسمح لك بتحديد متى يجب إعادة رسم القائمة. لكن حتى مع استخدام extraData، يمكن أن تحدث إعادة رسم غير ضرورية إذا لم تكن حذراً في كيفية تحديث الـ State.
// مثال خاطئ: إعادة رسم جميع العناصر عند تحديث عنصر واحد
const [expandedItems, setExpandedItems] = useState({});
const toggleExpand = (id) => {
setExpandedItems({
...expandedItems,
[id]: !expandedItems[id]
}); // هذا يؤدي إلى إعادة رسم جميع العناصر
};
// الحل: استخدام مفاتيح فريدة لكل عنصر
const toggleExpand = (id) => {
setExpandedItems(prev => ({
...prev,
[id]: !prev[id]
}));
};
// في FlatList
<FlatList
extraData={expandedItems}
// ... بقية الخصائص
/>الصور هي أحد أكبر أسباب بطء تطبيقات React Native، خاصة عندما تكون عالية الدقة أو يتم تحميلها من الإنترنت. في أحد المشاريع، كان لدينا تطبيق يعرض صور منتجات بحجم ٢٠٠٠×٢٠٠٠ بكسل، وكان التطبيق يستهلك أكثر من ٥٠٠ ميجابايت من الذاكرة عند فتح شاشة تحتوي على ١٠ صور فقط. المشكلة ليست فقط في حجم الصور، بل أيضاً في كيفية تحميلها وعرضها. مثلاً، استخدام Image بدون تحديد حجم ثابت يمكن أن يؤدي إلى إعادة رسم المكون عدة مرات أثناء تحميل الصورة.
الحل الأول لهذه المشكلة هو استخدام مكتبة مثل react-native-fast-image، التي توفر تحميلاً أسرع للصور وتخزيناً مؤقتاً أكثر كفاءة من مكون Image الأصلي. في تجربتنا، قمنا باستبدال جميع مكونات Image بمكتبة fast-image، ووجدنا أن وقت تحميل الصور انخفض من ١.٢ ثانية إلى ٣٠٠ مللي، كما انخفض استهلاك الذاكرة بنسبة ٤٠٪. لكن حتى مع استخدام fast-image، هناك بعض الممارسات التي يجب اتباعها لتحسين الأداء أكثر.
import FastImage from 'react-native-fast-image';
const ProductImage = ({ uri }) => {
return (
<FastImage
style={{ width: 200, height: 200 }}
source={
uri: uri,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable
}
resizeMode={FastImage.resizeMode.cover}
/>
);
};
// التحميل المسبق للصور
FastImage.preload([
{ uri: 'https://example.com/image1.jpg' },
{ uri: 'https://example.com/image2.jpg' }
]);إدارة الـ State في تطبيقات React Native يمكن أن تصبح كابوساً للأداء إذا لم يتم التعامل معها بحذر. مثلاً، استخدام Redux بشكل غير مدروس يمكن أن يؤدي إلى إعادة رسم جميع المكونات عند تحديث جزء صغير من الـ State. في أحد المشاريع، كان لدينا تطبيق يستخدم Redux لإدارة حالة المستخدم، وكانت هناك مشكلة غريبة: عند تسجيل الدخول، كان التطبيق يتجمد لمدة ثانية كاملة قبل الانتقال إلى الشاشة الرئيسية. بعد تحليل الأداء، اكتشفنا أن المشكلة تكمن في كيفية تحديث الـ State في Redux. كنا نستخدم مكتبة immutability-helper لتحديث الـ State، والتي كانت تؤدي إلى إنشاء نسخ جديدة من جميع الكائنات في الـ State حتى لو تم تحديث جزء صغير منها.
الحل لهذه المشكلة كان استخدام مكتبة مثل Immer، التي تسمح لك بكتابة كود متحور (mutating) بينما تقوم في الخلفية بإنشاء نسخة جديدة من الـ State. هذا التغيير وحده قلل وقت تحديث الـ State من ٨٠٠ مللي إلى ١٥٠ مللي، كما قلل عدد إعادة رسم المكونات من ٤٥ إلى ١٢. لكن حتى مع استخدام Immer، هناك بعض الممارسات التي يجب اتباعها لتحسين أداء إدارة الـ State أكثر.
import produce from 'immer';
const reducer = (state, action) => {
return produce(state, draft => {
switch (action.type) {
case 'UPDATE_USER':
draft.user.name = action.payload.name;
// لا حاجة لكتابة return draft، Immer تتعامل معها
break;
case 'ADD_PRODUCT':
draft.products.push(action.payload);
break;
}
});
};
// مقارنة مع immutability-helper
const newState = update(state, {
user: { name: { $set: action.payload.name } }
}); // يؤدي إلى إنشاء نسخ جديدة لكل الكائناتهناك مشكلة أخرى شائعة مع إدارة الـ State وهي استخدام Context API بشكل غير مدروس. على الرغم من أن Context هو حل رائع لتجنب تمرير الـ Props عبر عدة مستويات من المكونات، إلا أنه يمكن أن يؤدي إلى إعادة رسم جميع المكونات التي تستهلك هذا الـ Context عند تحديثه. مثلاً، إذا كان لديك Context يحتوي على حالة المستخدم، وكل مرة يقوم المستخدم بتحديث اسمه، يتم إعادة رسم جميع المكونات التي تستهلك هذا الـ Context، حتى لو لم تستخدم خاصية الاسم. الحل لهذه المشكلة هو تقسيم الـ Context إلى عدة Contexts أصغر، بحيث يحتوي كل Context على جزء محدد من الـ State.
// تقسيم الـ Context إلى عدة Contexts
const UserC React.createContext();
const SettingsContext = React.createContext();
const CartContext = React.createContext();
// بدلاً من استخدام Context واحد كبير
const AppContext = React.createContext();في بعض الأحيان، لا يكفي تحسين كود جافاسكربت وحده لتحسين أداء التطبيق. مثلاً، إذا كان لديك عملية حسابية معقدة أو معالجة للصور، فإن تنفيذها في جافاسكربت يمكن أن يؤدي إلى تجميد واجهة المستخدم. في هذه الحالات، الحل هو استخدام الوحدات الأصلية (Native Modules) التي تسمح لك بكتابة كود بلغة أصلية مثل جافا أو سويفت، ثم استدعاء هذا الكود من جافاسكربت.
في أحد المشاريع، كان لدينا تطبيق يحتاج إلى معالجة صور عالية الدقة قبل عرضها على الشاشة. عند تنفيذ هذه العملية في جافاسكربت، كان التطبيق يتجمد لمدة ٣ ثوانٍ كاملة. قمنا بنقل هذه العملية إلى وحدة أصلية مكتوبة بلغة جافا، ووجدنا أن وقت المعالجة انخفض من ٣٠٠٠ مللي إلى ٣٠٠ مللي فقط. الفرق هنا ليس فقط في سرعة التنفيذ، بل أيضاً في حقيقة أن الكود الأصلي يعمل في خيط منفصل (Thread) عن خيط واجهة المستخدم، مما يمنع تجميد التطبيق.
// Native Module في Android (Java)
package com.example.imagemodule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
public class ImageProcessorModule extends ReactContextBaseJavaModule {
ImageProcessorModule(ReactApplicationContext context) {
super(context);
}
@Override
public String getName() {
return "ImageProcessor";
}
@ReactMethod
public void processImage(String imagePath, Promise promise) {
try {
// معالجة الصورة باستخدام مكتبات أصلية
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
Bitmap processedBitmap = processBitmap(bitmap);
String resultPath = saveBitmap(processedBitmap);
promise.resolve(resultPath);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
private Bitmap processBitmap(Bitmap bitmap) {
// تنفيذ الخوارزمية الأصلية هنا
return bitmap;
}
private String saveBitmap(Bitmap bitmap) {
// حفظ الصورة المعالجة
return "path/to/processed/image";
}
}// استخدام Native Module في React Native
import { NativeModules } from 'react-native';
const { ImageProcessor } = NativeModules;
const processImage = async (imagePath) => {
try {
const result = await ImageProcessor.processImage(imagePath);
console.log('Processed image path:', result);
} catch (e) {
console.error('Error processing image:', e);
}
};أحد أكبر المفاهيم الخاطئة عن React Native هو الاعتقاد بأن تحسين أداء جافاسكربت وحده يكفي لتحسين أداء التطبيق. الحقيقة هي أن التطبيق يتكون من خيطين رئيسيين: خيط جافاسكربت وخيط أصلي (Native Thread). خيط جافاسكربت مسؤول عن تنفيذ كود جافاسكربت وإدارة الـ State، بينما خيط الأصلي مسؤول عن رسم واجهة المستخدم والتعامل مع أحداث النظام. إذا كان أحد الخيطين بطيئاً، سيتأثر أداء التطبيق بأكمله، حتى لو كان الخيط الآخر سريعاً.
في أحد المشاريع، كان لدينا تطبيق يعرض خريطة مع عدة علامات (Markers). عند التمرير السريع على الخريطة، كان التطبيق يتجمد لمدة ثانية كاملة. بعد تحليل الأداء باستخدام Flipper، اكتشفنا أن المشكلة ليست في كود جافاسكربت، بل في كيفية تعامل الخيط الأصلي مع أحداث التمرير. كان الخيط الأصلي يرسل أحداث التمرير إلى خيط جافاسكربت بشكل متكرر جداً، مما يؤدي إلى ازدحام في قائمة الأحداث (Event Queue). الحل لهذه المشكلة كان استخدام خاصية debounce في أحداث التمرير، بحيث لا يتم إرسال الحدث إلى جافاسكربت إلا بعد توقف المستخدم عن التمرير لمدة معينة.
import { debounce } from 'lodash';
const MapScreen = () => {
const handleRegi debounce((region) => {
// تحديث الـ State هنا
console.log('Region changed:', region);
}, 200);
return (
<MapView
onRegionChange={handleRegionChange}
// ... بقية الخصائص
/>
);
};بعد أكثر من عشر سنوات في تطوير تطبيقات الموبايل، وخمس سنوات منها مخصصة لـ React Native، هذه هي النصائح التي أتمنى لو عرفتها عندما بدأت. أولاً، لا تفترض أبداً أن المشكلة في مكان واحد؛ استخدم أدوات القياس لتحديد مكان المشكلة بالضبط قبل أن تبدأ في تحسين أي شيء. ثانياً، تحسين أداء الصور ليس ترفاً، بل ضرورة؛ استخدم مكتبات مثل react-native-fast-image وضغط الصور قبل رفعها. ثالثاً، لا تخف من استخدام الوحدات الأصلية عندما تحتاج إلى أداء أفضل؛ جافاسكربت ليس دائماً الحل الأمثل. رابعاً، إدارة الـ State بشكل صحيح يمكن أن يوفر عليك ساعات من العمل في تحسين الأداء لاحقاً؛ استخدم مكتبات مثل Immer لتقليل عدد إعادة رسم المكونات.
وأخيراً، تذكر أن تحسين الأداء ليس عملية واحدة تنتهي عند إطلاق التطبيق. مع كل تحديث جديد، ومع كل مكتبة جديدة تضيفها، يمكن أن تظهر مشاكل جديدة في الأداء. لهذا السبب، يجب أن تكون أدوات القياس جزءاً دائماً من سير عملك، وليس شيئاً تستخدمه فقط عندما تواجه مشكلة. إذا طبقت هذه النصائح بشكل صحيح، ستجد أن تحسين أداء تطبيقات React Native ليس مجرد تحسين للأرقام، بل تحسين لتجربة المستخدم بأكملها.