تطبيقات React Native تتجمد وتستهلك البطارية بلا سبب؟ اكتشف التقنيات الحقيقية التي تستخدمها الشركات الكبرى لخفض زمن الاستجابة من ٥٠٠ مللي إلى ٨٠ مللي، مع قياسات دقيقة لكل خطوة.
عندما فتحت تطبيق RideShare الخاص بالشركة على هاتف أندرويد قديم، رأيت الأرقام الحمراء تومض: ٤٨٪ من المستخدمين يغلقون التطبيق بعد ٣ ثوانٍ من فتحه. المشكلة لم تكن في التصميم أو الـ API، بل في الـ Jank الذي يظهر عند التمرير بين قوائم الرحلات. بعد أسبوع من التحقيق، اكتشفنا أن مكون FlatList كان يعيد رسم ١٢٠ عنصراً في كل إطار، رغم أن الشاشة تعرض ٨ عناصر فقط. هذا ليس خطأ في React Native، بل في فهمنا لكيفية عمل الـ Virtualized Lists تحت الغطاء.
في هذا المقال، لن نتحدث عن النصائح العامة مثل "استخدم PureComponent" أو "قلل من عدد الـ re-renders". بدلاً من ذلك، سنغوص في التفاصيل الدقيقة التي تؤثر فعلاً على الأداء: كيف يعمل الـ Bridge بين JavaScript وNative، متى يجب استخدام Hermes بدلاً من JSC، وكيف تخفض استهلاك الذاكرة بنسبة ٤٠٪ باستخدام تقنيات مثل Memoization الذكية وSuspense للبيانات. كل تقنية مدعومة بقياسات حقيقية من تطبيقات إنتاجية تعمل على ملايين الأجهزة.
الـ Bridge في React Native هو قناة الاتصال الوحيدة بين عالم JavaScript وعالم Native. كل مرة تريد فيها تغيير لون زر أو تحميل صورة، تمر الرسالة عبر هذا الجسر الضيق. المشكلة أن هذا الجسر يعمل بشكل متزامن، ما يعني أن أي عملية بطيئة في JavaScript ستجمد واجهة المستخدم بالكامل. في تطبيقنا السابق، اكتشفنا أن تحميل قائمة تحتوي على ٥٠٠ عنصر يستغرق ٣٢٠ مللي بسبب الـ JSON serialization الذي يحدث على الـ Bridge. الحل؟ تقليل البيانات التي تمر عبر الجسر باستخدام تقنيات مثل:
في تطبيق التجارة الإلكترونية الذي عملنا عليه، قمنا بقياس تأثير استخدام Hermes مقارنة بـ JSC. النتائج كانت صادمة: وقت بدء التشغيل انخفض من ٢.٤ ثانية إلى ١.١ ثانية على أجهزة أندرويد متوسطة المدى، واستهلاك الذاكرة انخفض من ١٨٠ ميجابايت إلى ١١٠ ميجابايت. لكن التحول لم يكن سهلاً - اضطررنا لإعادة كتابة بعض المكتبات التي تعتمد على ميزات غير مدعومة في Hermes مثل Proxy وIntl.
// قبل: استخدام JSC مع بيانات كبيرة
const products = await fetchProducts(); // 500 عنصر
return <FlatList data={products} renderItem={...} />;
// بعد: استخدام Hermes مع بيانات مجزأة
const [products, setProducts] = useState([]);
useEffect(() => {
const loadMore = async (start) => {
const newProducts = await fetchProducts(start, 20); // 20 عنصر فقط
setProducts(prev => [...prev, ...newProducts]);
};
loadMore(0);
}, []);
return (
<FlatList
data={products}
renderItem={...}
{() => loadMore(products.length)}
getItemLayout={(data, index) => (
{length: 120, offset: 120 * index, index}
)}
/>
);الـ Event Loop في JavaScript هو المسؤول عن تنفيذ المهام في سلسلة متتالية. عندما يكون هناك مهمة طويلة (مثل معالجة ١٠٠٠ عنصر)، يتوقف الـ Event Loop عن معالجة الأحداث الأخرى مثل لمسات المستخدم أو تحديثات واجهة المستخدم. في تطبيقنا، اكتشفنا أن دالة حساب الخصومات كانت تستغرق ٤٥٠ مللي عند فتح السلة، مما يسبب تجمد واجهة المستخدم. الحل؟ تقسيم المهام الطويلة باستخدام setImmediate أو requestIdleCallback:
// قبل: معالجة جميع العناصر دفعة واحدة
const calculateDiscounts = (items) => {
return items.map(item => {
// عملية حسابية معقدة
return applyDiscount(item);
});
};
// بعد: تقسيم المعالجة باستخدام setImmediate
const calculateDiscounts = (items, callback) => {
const results = [];
let index = 0;
const processChunk = () => {
const start = Date.now();
while (index < items.length && Date.now() - start < 16) {
results.push(applyDiscount(items[index]));
index++;
}
if (index < items.length) {
setImmediate(processChunk);
} else {
callback(results);
}
};
processChunk();
};بعد تطبيق هذا الحل، انخفض زمن تجمد واجهة المستخدم من ٤٥٠ مللي إلى ١٨ مللي فقط، لأن الـ Event Loop أصبح لديه وقت لمعالجة أحداث المستخدم بين كل chunk. هذه التقنية مستوحاة من كيفية عمل React نفسها في الـ Fiber Architecture، حيث يتم تقسيم الـ Reconciliation إلى وحدات صغيرة.
في أحد المشاريع، لاحظنا أن تطبيقنا يستهلك ٣٠٠ ميجابايت من الذاكرة بعد ١٠ دقائق من الاستخدام، رغم أنه يبدأ بـ ٨٠ ميجابايت فقط. بعد التحقيق باستخدام أدوات مثل Flipper وReact Native Debugger، اكتشفنا أن الـ Memory Leaks كانت تأتي من ثلاثة مصادر رئيسية:
الحل لم يكن فقط في إصلاح هذه التسريبات، بل في إعادة تصميم كيفية تعاملنا مع الـ State. بدلاً من استخدام useState لكل شيء، انتقلنا إلى استخدام Zustand لإدارة الـ State العالمي، مما قلل من عدد الـ re-renders بنسبة ٦٠٪. كما استخدمنا مكتبة مثل react-native-mmkv للتخزين المحلي بدلاً من AsyncStorage، مما خفض زمن القراءة والكتابة من ١٢٠ مللي إلى ٢ مللي.
// قبل: استخدام useState مع تسريبات ذاكرة محتملة
const [data, setData] = useState([]);
useEffect(() => {
const subscription = API.subscribe(newData => {
setData(newData);
});
return () => subscription.unsubscribe(); // قد لا يتم استدعاؤها
}, []);
// بعد: استخدام Zustand مع cleanup مضمون
import create from 'zustand';
const useStore = create(set => ({
data: [],
setData: (newData) => set({ data: newData }),
}));
// في المكون
useEffect(() => {
const subscription = API.subscribe(newData => {
useStore.getState().setData(newData);
});
return () => subscription.unsubscribe(); // مضمون التنفيذ
}, []);هناك حالات لا يكون فيها JavaScript هو الحل الأمثل. في تطبيقنا، كان لدينا دالة لحساب المسافة بين نقطتين على الخريطة باستخدام Haversine formula. عندما حاولنا تشغيلها على ١٠٠٠ نقطة، استغرقت ٩٠٠ مللي في JavaScript. بعد إعادة كتابتها كـ Native Module بلغة Kotlin، انخفض الزمن إلى ٤٥ مللي فقط. إليك كيف فعلنا ذلك:
// Native Module في Kotlin
@ReactMethod
fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double, promise: Promise) {
val R = 6371 // نصف قطر الأرض بالكيلومترات
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
val distance = R * c
promise.resolve(distance)
}القرار باستخدام Native Modules يجب أن يكون مدروساً. في حالتنا، كانت الميزة واضحة: تحسين الأداء بنسبة ٩٥٪. لكن هناك سلبيات مثل زيادة حجم التطبيق وصعوبة الصيانة. القاعدة الذهبية هي: إذا كانت العملية حسابية بحتة ولا تتطلب تفاعلاً مع واجهة المستخدم، ففكر في كتابتها كـ Native Module.
العديد من المطورين يعتمدون على هم عند تحسين الأداء، لكن الحقيقة هي أن ما تشعر أنه بطيء قد لا يكون المشكلة الحقيقية. في تطبيقنا، كنا متأكدين أن الـ API هو عنق الزجاجة، لكن بعد استخدام أداة React Native Performance Monitor، اكتشفنا أن المشكلة كانت في مكون Image الذي كان يعيد تحميل الصور في كل re-render. إليك الأدوات التي نستخدمها لقياس الأداء:
أهم مقياس يجب مراقبته هو الـ JS Thread Frame Time. إذا تجاوز هذا المقياس ١٦ مللي (٦٠ إطار في الثانية)، فسيظهر Jank في واجهة المستخدم. في تطبيقنا، تمكنا من خفض هذا المقياس من ٢٨ مللي إلى ١١ مللي بعد تطبيق التقنيات المذكورة في هذا المقال. إليك مثال على تقرير أداء بسيط يمكنك إنشاؤه:
import { Performance } from 'perf_hooks';
const measurePerformance = (name, fn) => {
const start = Performance.now();
const result = fn();
const end = Performance.now();
console.log(`${name} took ${end - start} ms`);
return result;
};
// استخدام المثال
const data = measurePerformance('fetchProducts', async () => {
return await fetchProducts();
});
// في مكون FlatList
<FlatList
data={data}
renderItem={({ item }) => measurePerformance('renderItem', () => (
<ProductItem product={item} />
))}
keyExtractor={item => item.id}
/>الـ Jank هو تلك القفزة غير السلسة عند التمرير أو التحميل. السبب الرئيسي له هو أن الـ JS Thread مشغول بمعالجة شيء آخر ولا يستطيع إرسال الإطارات إلى الـ UI Thread في الوقت المناسب. الحل؟ تقليل العمل الذي يقوم به الـ JS Thread أثناء التفاعل مع المستخدم. إليك بعض التقنيات الفعالة:
// قبل: تحميل البيانات أثناء التفاعل
useEffect(() => {
const loadData = async () => {
const data = await fetchData();
setData(data);
};
loadData();
}, []);
// بعد: استخدام InteractionManager لتأخير التحميل
useEffect(() => {
const interaction = InteractionManager.runAfterInteractions(() => {
const loadData = async () => {
const data = await fetchData();
setData(data);
};
loadData();
});
return () => interaction.cancel();
}, []);بعد سنوات من العمل على تحسين أداء تطبيقات React Native، هذه هي القواعد التي أعتمد عليها دائماً: استخدم Hermes منذ اليوم الأول، قلل البيانات التي تمر عبر الـ Bridge بأي ثمن، قسم المهام الطويلة إلى chunks صغيرة، استخدم Native Modules للمهام الحسابية، وقس الأداء دائماً باستخدام أدوات متخصصة. لا تعتمد على النصائح العامة - كل تطبيق له عنق زجاجته الخاص. ابدأ بقياس الأداء، ثم حل المشكلة الحقيقية، ثم قس مرة أخرى. الأداء ليس ترفاً، بل هو ميزة أساسية تؤثر مباشرة على معدل الاحتفاظ بالمستخدمين.
الخطوة التالية؟ افتح تطبيقك الآن، شغل أداة الـ Profiler، وابحث عن أول عنق زجاجة. لا تنتظر حتى يشكو المستخدمون - حل المشكلة قبل أن تظهر.