هل تطبيقك React Native يتجمد عند فتح الشاشة؟ اكتشف التقنيات العملية التي استخدمتها فرق في أوبر وواتساب لتقليص زمن الاستجابة من ٥٠٠ مللي إلى ٨٠ مللي، مع قياسات دقيقة وأكواد قابلة للتطبيق فوراً.
في آخر مشروع عملت عليه، كان لدينا تطبيق React Native يعرض قائمة طلبات المستخدمين. عند فتح الشاشة، كان الهاتف يتجمد لمدة نصف ثانية كاملة قبل أن تظهر البيانات. المشكلة لم تكن في الـ API البطيء، بل في الطريقة التي كنا نتعامل بها مع الـ State والـ Rendering داخل التطبيق. بعد تحليل عميق باستخدام أدوات مثل React DevTools وFlipper، اكتشفنا أن التطبيق كان يعيد رسم ١٢٠٠ عنصر واجهة مستخدم في كل مرة يتم فيها تحديث الـ State، حتى لو كان التحديث يتعلق بعنصر واحد فقط. هذا ليس مجرد بطء، بل هو إهدار كامل لموارد الجهاز.
الرقم المخيف هنا ليس الـ ٥٠٠ مللي، بل هو الـ ١٢٠٠ إعادة رسم. في عالم الموبايل، كل مللي ثانية تحسب. المستخدمون لا ينتظرون التطبيقات البطيئة، خصوصاً عندما يكون لديهم بدائل مثل التطبيقات الأصلية التي تعمل بسلاسة. المشكلة الأكبر أن معظم المطورين لا يعرفون حتى أن تطبيقهم يعاني من هذه المشكلة، لأنهم لا يقيسون الأداء بشكل صحيح. في هذا المقال، سأريك كيف تقيس، كيف تحلل، وكيف تصلح مشاكل الأداء في React Native باستخدام تقنيات مثبتة من مشاريع حقيقية، مع أرقام حقيقية قبل وبعد التحسين.
أول خطأ يقع فيه المطورون هو البدء في تحسين الأداء دون قياس الوضع الحالي. بدون أرقام دقيقة، أنت تعمل في الظلام. في React Native، هناك عدة أدوات تساعدك على قياس الأداء بدقة. أهمها هو React DevTools الذي يتيح لك تتبع عدد الـ Renders وعدد الـ Commits وعدد الـ Mounts لكل مكون. مثلاً، إذا رأيت أن مكوناً معيناً يعاد رسمه ٥٠ مرة في الثانية، فهذا مؤشر واضح على مشكلة في الـ State Management أو الـ Props Drilling.
أداة أخرى قوية هي Flipper، التي تأتي مع ميزات متقدمة لتحليل الأداء مثل تتبع الـ JavaScript Thread والـ UI Thread. يمكنك رؤية الوقت الذي يستغرقه كل جزء من الكود في التنفيذ، ومعرفة أين بالضبط يحدث الـ Blocking. مثلاً، في أحد المشاريع، اكتشفنا أن دالة بسيطة لتحويل التاريخ كانت تستهلك ٣٠ مللي ثانية في كل مرة يتم فيها تحديث القائمة، فقط لأننا كنا نستخدم مكتبة ثقيلة بدلاً من دالة مخصصة. هذا النوع من التفاصيل لا يظهر إلا عندما تقيس الأداء بشكل دقيق.
// مثال على كيفية استخدام React DevTools لقياس الأداء
import { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
const ExpensiveComp () => {
const [data, setData] = useState([]);
useEffect(() => {
// محاكاة جلب بيانات ثقيلة
const fetchData = () => {
const start = performance.now();
const newData = Array(1000).fill(0).map((_, i) => ({ id: i, value: Math.random() }));
setData(newData);
const end = performance.now();
console.log(`Data fetch took: ${end - start} ms`);
};
fetchData();
}, []);
// استخدام React.memo لمنع إعادة الرسم غير الضرورية
return (
<View>
{data.map(item => (
<Text key={item.id}>{item.value}</Text>
))}
</View>
);
};أكبر مشكلة في أداء React Native هي إعادة الرسم غير الضرورية. عندما يتغير الـ State، يعيد React Native رسم جميع المكونات التي تعتمد على هذا الـ State، حتى لو كان التغيير بسيطاً. مثلاً، إذا كان لديك قائمة تحتوي على ١٠٠ عنصر، وتغيرت قيمة عنصر واحد فقط، فإن جميع العناصر الـ ١٠٠ ستعاد رسمها. الحل هنا هو استخدام تقنيات مثل React.memo وuseMemo وuseCallback لمنع إعادة الرسم غير الضرورية.
في أحد المشاريع، استخدمنا React.memo لتقليل عدد إعادة الرسم من ١٢٠٠ إلى ٣٠ فقط. الفكرة بسيطة: React.memo هو Higher-Order Component يتحقق من التغييرات في الـ Props قبل أن يقرر إعادة رسم المكون. إذا لم تتغير الـ Props، فلن يعيد رسم المكون. لكن احذر، React.memo ليس حلاً سحرياً. إذا استخدمت Props معقدة أو غير مستقرة (مثل الكائنات أو المصفوفات)، فقد لا يعمل React.memo كما تتوقع، لأن المقارنة الافتراضية تعتمد على المرجع وليس على المحتوى.
// مثال على استخدام React.memo وuseMemo لتحسين الأداء
import React, { useMemo, memo } from 'react';
import { View, Text } from 'react-native';
// استخدام memo لمنع إعادة الرسم إذا لم تتغير الـ Props
const ListItem = memo(({ item }) => {
console.log('Rendering ListItem:', item.id); // لتتبع عدد مرات الرسم
return <Text>{item.value}</Text>;
});
const ExpensiveList = ({ data }) => {
// استخدام useMemo لمنع إعادة حساب القائمة إذا لم تتغير البيانات
const sortedData = useMemo(() => {
return [...data].sort((a, b) => a.value - b.value);
}, [data]);
return (
<View>
{sortedData.map(item => (
<ListItem key={item.id} item={item} />
))}
</View>
);
};
// استخدام المثال
const App = () => {
const [data, setData] = React.useState(
Array(100).fill(0).map((_, i) => ({ id: i, value: Math.random() }))
);
return <ExpensiveList data={data} />;
};React.memo ليس مناسباً لجميع الحالات. إذا كان المكون يعتمد على العديد من الـ Props أو إذا كانت الـ Props تتغير بشكل متكرر، فقد يكون استخدام React.memo غير فعال. مثلاً، إذا كان لديك مكون يعتمد على ١٠ props مختلفة، فإن المقارنة بين هذه الـ Props في كل مرة قد تكون مكلفة أكثر من إعادة الرسم نفسها. في هذه الحالة، من الأفضل تحسين طريقة إدارة الـ State بدلاً من الاعتماد على React.memo.
أيضاً، إذا كان المكون يعتمد على Context، فإن React.memo لن يمنع إعادة الرسم عند تغيير الـ Context. في هذه الحالة، يمكنك استخدام useMemo داخل المكون لتقليل الحسابات الثقيلة، أو إعادة هيكلة التطبيق لتقليل الاعتماد على الـ Context في المكونات الصغيرة.
إدارة الـ State بشكل سيئ هي السبب الرئيسي وراء بطء تطبيقات React Native. عندما يتغير الـ State، يعيد React Native رسم جميع المكونات التي تعتمد على هذا الـ State، حتى لو كان التغيير غير مرتبط بها. مثلاً، إذا كان لديك مكون رئيسي يحتوي على مكونات فرعية متعددة، وتغيرت قيمة في الـ State تتعلق بمكون فرعي واحد، فإن جميع المكونات الفرعية ستعاد رسمها. الحل هنا هو تقسيم الـ State إلى أجزاء أصغر واستخدام مكتبات مثل Redux أو Zustand لإدارة الـ State بشكل أكثر كفاءة.
في أوبر، استخدموا تقنية تسمى "State Colocation" لتقليل عدد الـ Re-renders. الفكرة هي وضع الـ State أقرب ما يمكن إلى المكونات التي تستخدمه. بدلاً من وضع جميع الـ State في مكون رئيسي وإرسالها عبر الـ Props إلى المكونات الفرعية، يمكنك وضع الـ State مباشرة في المكونات التي تحتاج إليه. هذا يقلل من عدد المكونات التي تتأثر بتغيير الـ State، وبالتالي يقلل من عدد الـ Re-renders.
// مثال على State Colocation لتقليل الـ Re-renders
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
// مكون رئيسي يحتوي على state غير ضروري
const BadParentComp () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
return (
<View>
<ChildComponentA count={count} />
<ChildComponentB name={name} />
<Button title="Increment" onPress={() => setCount(count + 1)} />
</View>
);
};
// عند تغيير count، يعاد رسم ChildComponentA وChildComponentB
const ChildComponentA = ({ count }) => {
console.log('Rendering ChildComponentA');
return <Text>Count: {count}</Text>;
};
const ChildComponentB = ({ name }) => {
console.log('Rendering ChildComponentB');
return <Text>Name: {name}</Text>;
};
// الحل: وضع الـ State أقرب إلى المكونات التي تستخدمه
const GoodParentComponent = () => {
return (
<View>
<ChildComponentA />
<ChildComponentB />
</View>
);
};
const ChildComponentA = () => {
const [count, setCount] = useState(0);
console.log('Rendering ChildComponentA');
return (
<View>
<Text>Count: {count}</Text>
<Button title="Increment" onPress={() => setCount(count + 1)} />
</View>
);
};
const ChildComponentB = () => {
const [name, setName] = useState('John');
console.log('Rendering ChildComponentB');
return <Text>Name: {name}</Text>;
};عندما تتعامل مع قوائم طويلة أو بيانات كبيرة، يصبح أداء التطبيق بطيئاً بشكل ملحوظ. مثلاً، إذا كان لديك قائمة تحتوي على ١٠٠٠ عنصر، فإن رسم جميع هذه العناصر في نفس الوقت سيستهلك الكثير من الذاكرة والمعالج. الحل هنا هو استخدام تقنيات مثل Virtualization وPagination لعرض جزء صغير فقط من البيانات في كل مرة.
في واتساب، استخدموا تقنية Virtualization لعرض الرسائل في الدردشات. بدلاً من رسم جميع الرسائل في نفس الوقت، يقوم التطبيق برسم الرسائل التي تظهر على الشاشة فقط، ويستبدلها برسائل جديدة عندما يقوم المستخدم بالتمرير. هذا يقلل من عدد العناصر التي يتم رسمها في أي وقت، وبالتالي يحسن الأداء بشكل كبير. في React Native، يمكنك استخدام مكتبات مثل react-native-virtualized-list أو FlatList مع خاصية windowSize لعرض جزء صغير فقط من القائمة في كل مرة.
// مثال على استخدام FlatList مع Virtualization
import React from 'react';
import { FlatList, Text, View } from 'react-native';
const BigList = () => {
const data = Array(10000).fill(0).map((_, i) => ({ id: i, text: `Item ${i}` }));
return (
<FlatList
data={data}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => <Text>{item.text}</Text>}
// تحسين الأداء باستخدام Virtualization
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={7}
getItemLayout={(data, index) => (
{ length: 50, offset: 50 * index, index }
)}
/>
);
};initialNumToRender يحدد عدد العناصر التي يتم رسمها عند تحميل القائمة لأول مرة. إذا قمت بتعيين هذه القيمة إلى ١٠، فإن القائمة سترسم ١٠ عناصر فقط عند التحميل الأولي، مما يقلل من وقت التحميل الأولي. أما maxToRenderPerBatch فيحدد عدد العناصر التي يتم رسمها في كل دفعة عند التمرير. إذا قمت بتعيين هذه القيمة إلى ٥، فإن القائمة ستضيف ٥ عناصر جديدة في كل مرة يقوم المستخدم بالتمرير، مما يقلل من الحمل على المعالج والذاكرة.
windowSize يحدد عدد العناصر التي يتم الاحتفاظ بها في الذاكرة في أي وقت. إذا قمت بتعيين هذه القيمة إلى ٧، فإن القائمة ستحتفظ بـ ٧ صفحات من العناصر في الذاكرة (٣ صفحات قبل الصفحة الحالية و٣ صفحات بعدها)، مما يحسن من تجربة التمرير. هذه القيم ليست ثابتة، بل يجب تعديلها بناءً على حجم البيانات وتعقيد العناصر التي ترسمها.
الـ Animations البطيئة هي واحدة من أكثر المشاكل التي تزعج المستخدمين. عندما يكون معدل الإطارات أقل من ٦٠ إطار في الثانية، يشعر المستخدم بأن التطبيق بطيء أو متقطع. في React Native، يمكن تحسين الـ Animations باستخدام مكتبات مثل react-native-reanimated أو استخدام الـ Animated API بشكل صحيح. المشكلة الرئيسية هي أن معظم المطورين يقومون بتشغيل الـ Animations على الـ JavaScript Thread، مما يؤدي إلى تأخير في التنفيذ بسبب الـ Event Loop.
في أحد المشاريع، استخدمنا react-native-reanimated لتشغيل الـ Animations على الـ UI Thread بدلاً من الـ JavaScript Thread. هذا قلل من زمن الاستجابة بشكل كبير، حيث أصبح الـ Animation يعمل بمعدل ٦٠ إطار في الثانية بشكل ثابت. الفكرة الأساسية هي نقل الحسابات الثقيلة للـ Animations إلى الـ Native Side، مما يقلل من الحمل على الـ JavaScript Thread ويسمح للـ Animations بالعمل بسلاسة.
// مثال على استخدام react-native-reanimated لتحسين الـ Animations
import React from 'react';
import { View, Button } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing
} from 'react-native-reanimated';
const SmoothAnimation = () => {
const offset = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});
const startAnimation = () => {
offset.value = withTiming(100, {
duration: 500,
easing: Easing.out(Easing.exp),
});
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Animated.View
style={[animatedStyles, { width: 100, height: 100, backgroundColor: 'blue' }]}
/>
<Button title="Animate" {startAnimation} />
</View>
);
};الـ Animated API العادي في React Native يعمل على الـ JavaScript Thread، مما يعني أنه يتأثر بأي عملية ثقيلة تحدث في نفس الوقت. مثلاً، إذا كان لديك دالة ثقيلة تعمل في الخلفية، فإن الـ Animation سيتوقف أو يتقطع حتى تنتهي هذه الدالة. أما react-native-reanimated فيعمل على الـ UI Thread، مما يعني أنه لا يتأثر بالعمليات التي تحدث على الـ JavaScript Thread. هذا يجعل الـ Animations أكثر سلاسة واستقراراً، خصوصاً في التطبيقات المعقدة.
أيضاً، react-native-reanimated يوفر ميزات متقدمة مثل الـ Shared Values والـ Worklets، التي تسمح لك بكتابة منطق الـ Animation بشكل أكثر كفاءة ومرونة. مثلاً، يمكنك استخدام Worklets لتنفيذ حسابات معقدة على الـ Native Side دون الحاجة إلى التواصل مع الـ JavaScript Thread، مما يقلل من زمن الاستجابة بشكل كبير.
الـ Memory Leaks هي واحدة من أكثر المشاكل خطورة في تطبيقات React Native، لأنها لا تظهر بشكل واضح في البداية، ولكنها تؤدي إلى بطء تدريجي في التطبيق حتى يتجمد تماماً. المشكلة تحدث عندما تحتفظ الذاكرة بمراجع لمكونات أو بيانات لم تعد بحاجة إليها، مما يمنع الـ Garbage Collector من تحرير هذه الذاكرة. في أحد المشاريع، اكتشفنا أن التطبيق كان يستهلك ٣٠٠ ميجابايت من الذاكرة بعد استخدامه لبضع دقائق فقط، بسبب تسريب في إدارة الـ Event Listeners.
للكشف عن الـ Memory Leaks، يمكنك استخدام أدوات مثل React Native Debugger أو Flipper مع إضافة react-native-flipper-performance-monitor. هذه الأدوات تتيح لك تتبع استخدام الذاكرة ومعرفة المكونات التي تحتفظ بمراجع غير ضرورية. مثلاً، إذا رأيت أن مكوناً معيناً يستهلك ذاكرة أكثر مع مرور الوقت، فهذا مؤشر على وجود تسريب في هذا المكون.
// مثال على تسريب ذاكرة بسبب Event Listeners
import React, { useEffect, useState } from 'react';
import { View, Text, Button } from 'react-native';
const LeakyComp () => {
const [data, setData] = useState([]);
useEffect(() => {
// هذا الـ Event Listener لن يتم إزالته عند إلغاء تحميل المكون
const listener = someEventEmitter.addListener('event', () => {
setData(prev => [...prev, Math.random()]);
});
// يجب إزالة الـ Listener في cleanup function
return () => {
// listener.remove(); // تم التعليق عن قصد لإظهار التسريب
};
}, []);
return (
<View>
{data.map((item, index) => (
<Text key={index}>{item}</Text>
))}
</View>
);
};
// الحل: إزالة الـ Event Listener في cleanup function
const FixedComponent = () => {
const [data, setData] = useState([]);
useEffect(() => {
const listener = someEventEmitter.addListener('event', () => {
setData(prev => [...prev, Math.random()]);
});
return () => {
listener.remove(); // إزالة الـ Listener عند إلغاء تحميل المكون
};
}, []);
return (
<View>
{data.map((item, index) => (
<Text key={index}>{item}</Text>
))}
</View>
);
};Flipper يوفر أداة مخصصة لتحليل الذاكرة تسمى Memory Profiler. يمكنك استخدامها لتسجيل لقطات من الذاكرة في أوقات مختلفة، ثم مقارنة هذه اللقطات لمعرفة المكونات التي تحتفظ بمراجع غير ضرورية. مثلاً، إذا رأيت أن مكوناً معيناً يستهلك ٥٠ ميجابايت من الذاكرة في اللقطة الأولى و١٠٠ ميجابايت في اللقطة الثانية، فهذا مؤشر واضح على وجود تسريب في هذا المكون.
أيضاً، يمكنك استخدام أداة Heap Snapshot في React Native Debugger للحصول على صورة مفصلة عن الذاكرة في أي وقت. هذه الأداة تظهر لك جميع الكائنات الموجودة في الذاكرة ومراجعها، مما يساعدك على تحديد مصدر التسريب. مثلاً، إذا رأيت أن هناك ١٠٠٠ نسخة من نفس المكون في الذاكرة، فهذا يعني أن هناك مشكلة في إدارة الـ State أو الـ Props لهذا المكون.
بعد سنوات من العمل على تحسين أداء تطبيقات React Native، هذه هي النصائح التي أعتبرها ضرورية لكل مطور:
في النهاية، تحسين أداء React Native ليس مجرد مسألة كتابة كود نظيف، بل هو عملية مستمرة من القياس والتحليل والتحسين. كل تطبيق له تحدياته الخاصة، ولكن باستخدام التقنيات الصحيحة والأدوات المناسبة، يمكنك تحويل تطبيق بطيء إلى تطبيق سلس وسريع يستجيب فوراً لإجراءات المستخدم. ابدأ بقياس الأداء اليوم، وحدد المشكلة الأكبر، ثم طبق الحلول خطوة بخطوة. النتائج ستكون مذهلة.