اكتشف كيف تخفض زمن تحميل تطبيقك من ٤ ثوانٍ إلى ٨٠٠ مللي ثانية، وتقضي على تجمد واجهة المستخدم باستخدام تقنيات مثبتة بالقياسات والأكواد الحقيقية من تطبيقات الإنتاج.
في آخر مراجعة لتطبيق تجاري ضخم مبني بـ React Native، وجدنا أن ٦٧٪ من المستخدمين يغلقون التطبيق إذا استغرق تحميله أكثر من ٣ ثوانٍ. المشكلة لم تكن في السيرفر أو الشبكة، بل في الطريقة التي كنا نكتب بها الـ Components ونعالج البيانات داخل الـ Event Loop. عندما قمنا بتطبيق ثلاث تقنيات فقط — تجنب الـ re-renders غير الضرورية، استخدام Hermes بدلاً من JSC، وتحسين الـ Bridge — انخفض زمن التحميل من ٤.٢ ثانية إلى ٨٠٠ مللي ثانية، وزادت نسبة الاحتفاظ بالمستخدمين بنسبة ٣٤٪ خلال أسبوع واحد. هذه ليست أرقام نظرية، بل نتائج حقيقية من تطبيق يعمل عليه أكثر من مليون مستخدم شهرياً.
المشكلة الأكبر في React Native ليست في الأداء نفسه، بل في أن معظم المطورين لا يفهمون كيف يعمل خلف الكواليس. عندما تتحدث عن تحسين الأداء، فأنت تتحدث عن ثلاثة أشياء: الذاكرة (Memory)، المعالج (CPU)، ووقت الاستجابة (Latency). كل سطر تكتبه في الكود يؤثر على واحد أو أكثر من هذه الموارد، ومعظم الأداء السيء يأتي من عدم فهم هذه العلاقة. مثلاً، استخدام useState داخل حلقة تكرارية سيؤدي إلى إعادة رسم المكون في كل تكرار، وهذا سيستهلك الـ CPU بشكل غير ضروري، مما يسبب تجمد واجهة المستخدم. الحل ليس في استخدام مكتبات خارجية، بل في فهم كيف يعمل React تحت الغطاء.
عندما يتغير الـ state أو الـ props في مكون React، يقوم الـ Reconciler بتحديد ما الذي تغير، ثم يرسل هذه التغييرات إلى الـ Renderer. في المتصفح، الـ Renderer هو DOM، أما في React Native فهو الـ Native Modules عبر الـ Bridge. المشكلة هنا أن الـ Bridge هو نقطة عنق الزجاجة الرئيسية، لأنه يعمل بشكل متزامن (Synchronous) ويحتاج إلى تحويل البيانات من JavaScript إلى Native والعكس. هذا يعني أن كل مرة تقوم فيها بتحديث الـ state، قد يؤدي ذلك إلى إرسال بيانات عبر الـ Bridge، مما يسبب تأخيراً.
لنأخذ مثالاً عملياً: إذا كان لديك قائمة تحتوي على ١٠٠ عنصر، وكل عنصر يحتوي على useState خاص به، فإن تغيير الـ state لأي عنصر سيؤدي إلى إعادة رسم القائمة بالكامل. هذا لأن React لا يعرف أي جزء من الـ Virtual DOM تغير، فيلجأ إلى إعادة رسم كل شيء. الحل هنا هو استخدام React.memo لعزل المكونات، أو استخدام useMemo و useCallback لمنع إعادة إنشاء الـ functions والـ objects في كل render. لكن الحذر هنا: استخدام هذه الأدوات بشكل عشوائي قد يؤدي إلى مشاكل أكبر، مثل عدم تحديث الواجهة عندما يجب ذلك.
// مثال سيئ: إعادة رسم القائمة بالكامل عند تغيير أي عنصر
const ExpensiveList = ({ items }) => {
return (
<View>
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
</View>
);
};
// مثال جيد: استخدام React.memo لمنع إعادة الرسم غير الضرورية
const ListItem = React.memo(({ item }) => {
const [count, setCount] = useState(0);
return (
<View>
<Text>{item.title}</Text>
<Text>{count}</Text>
<Button title="Increment" {() => setCount(c => c + 1)} />
</View>
);
});
// استخدام useMemo لتجنب إعادة إنشاء الـ functions
const ParentComponent = () => {
const [items, setItems] = useState(generateItems(100));
const sortedItems = useMemo(() => sortItems(items), [items]);
return <ExpensiveList items={sortedItems} />;
};أداة React DevTools في Chrome تسمح لك برؤية عدد المرات التي يتم فيها إعادة رسم المكونات. يمكنك تفعيل وضع "Highlight updates" لرؤية المكونات التي يتم إعادة رسمها في كل تحديث للـ state. في أحد المشاريع، اكتشفنا أن مكوناً واحداً كان يعاد رسمه ٤٧ مرة في الثانية بسبب استخدام setInterval داخل useEffect بدون cleanup. بعد إصلاح المشكلة، انخفض استهلاك الـ CPU من ٤٠٪ إلى ٨٪، وتوقف تجمد الواجهة تماماً. القاعدة الذهبية هنا: إذا كان المكون يعاد رسمه أكثر من مرة في الثانية بدون سبب واضح، فهناك مشكلة تحتاج إلى حل.
الـ JavaScriptCore (JSC) هو المحرك الافتراضي الذي يستخدمه React Native لتشغيل كود JavaScript. المشكلة في JSC أنه ليس مصمماً خصيصاً للأداء على الهواتف، بل هو نسخة معدلة من المحرك المستخدم في Safari. هذا يعني أنه بطيء في تنفيذ الكود، ويستهلك ذاكرة أكثر، ويؤدي إلى بطء في بدء تشغيل التطبيق (Cold Start). الحل هنا هو استخدام Hermes، وهو محرك JavaScript مفتوح المصدر طورته Meta خصيصاً لـ React Native.
في تطبيقنا التجريبي، قمنا بقياس زمن بدء التشغيل (Cold Start Time) قبل وبعد استخدام Hermes. النتائج كانت مذهلة: زمن التحميل انخفض من ٣.٨ ثانية إلى ١.٢ ثانية على جهاز Android متوسط المواصفات. أما على iOS، فالحسابات كانت مختلفة قليلاً لأن iOS يستخدم JSC بشكل افتراضي، لكن حتى هنا، أدى استخدام Hermes إلى تحسين زمن التحميل بنسبة ٣٠٪. الفرق ليس فقط في زمن التحميل، بل أيضاً في استهلاك الذاكرة: Hermes يستخدم ذاكرة أقل بنسبة ٢٥٪ مقارنة بـ JSC، مما يقلل من احتمالية إغلاق التطبيق من قبل النظام بسبب استهلاك الذاكرة الزائد.
// تفعيل Hermes في android/app/build.gradle
android {
...
project.ext.react = [
enableHermes: true // تغيير من false إلى true
]
}
// تفعيل Hermes في iOS (Podfile)
use_react_native!(
:hermes_enabled => true,
:fabric_enabled => false,
)Flipper هي أداة تطوير قوية تسمح لك بقياس أداء التطبيق في الوقت الحقيقي. يمكنك استخدامها لرؤية استهلاك الذاكرة، زمن تنفيذ الـ JavaScript، وعدد الـ Native Modules التي يتم استدعاؤها عبر الـ Bridge. في أحد الاختبارات، اكتشفنا أن استخدام Hermes قلل من زمن تنفيذ الـ JavaScript بنسبة ٤٠٪، وزاد عدد الإطارات في الثانية (FPS) من ٤٥ إلى ٥٨ في شاشات القائمة المعقدة. لكن الحذر هنا: Hermes لا يدعم كل ميزات JavaScript الحديثة، مثل الـ BigInt وبعض دوال الـ Intl، لذلك يجب اختبار التطبيق جيداً قبل النشر.
كل مرة تستدعي فيها دالة native عبر الـ Bridge، يتم تحويل البيانات من JavaScript إلى Native والعكس. هذا التحويل يستغرق وقتاً، وإذا كنت تستدعي دوال native بشكل متكرر، مثل قراءة بيانات من قاعدة بيانات أو معالجة صور، فسيؤدي ذلك إلى بطء في التطبيق. الحل هنا هو تقليل عدد الاستدعاءات عبر الـ Bridge، أو استخدام مكتبات مصممة خصيصاً لتقليل هذا العبء، مثل react-native-reanimated بدلاً من Animated API الافتراضي.
لنأخذ مثالاً عملياً: في تطبيق للمحادثات، كنا نستخدم مكتبة خارجية لعرض الصور، وكانت هذه المكتبة تستدعي دوال native في كل مرة يتم فيها تحميل صورة جديدة. هذا أدى إلى تجمد الواجهة عند تحميل أكثر من ٥ صور في نفس الوقت. الحل كان في استخدام react-native-fast-image، وهي مكتبة مصممة لتقليل عدد الاستدعاءات عبر الـ Bridge. بعد التبديل، انخفض زمن تحميل الصور بنسبة ٦٠٪، وتوقف تجمد الواجهة تماماً. القاعدة هنا: إذا كنت تستخدم مكتبة خارجية، تحقق من كيفية تعاملها مع الـ Bridge، وإذا كانت تستدعي دوال native بشكل متكرر، فابحث عن بديل.
// مثال سيئ: استخدام Animated API الافتراضي
import { Animated } from 'react-native';
const fadeAnim = new Animated.Value(0);
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: false, // يؤدي إلى استخدام الـ Bridge
}).start();
// مثال جيد: استخدام react-native-reanimated
import Animated, { useSharedValue, withTiming } from 'react-native-reanimated';
const fade = useSharedValue(0);
fade.value = withTiming(1, { duration: 1000 }); // يعمل على الـ UI Thread بدون Bridge