عندما يتجمد التطبيق عند فتح الشاشة الثالثة أو يستهلك البطارية كأنك تشغل لعبة AAA، تعرف أنك أمام مشكلة أداء حقيقية. هذا المقال يكشف لك تقنيات عملية مدعومة بقياسات فعلية لتحسين أداء تطبيقات React Native دون الحاجة لإعادة كتابة الكود من الصفر.
في أحد المشاريع الكبيرة لشركة سعودية للتجارة الإلكترونية، واجهنا مشكلة غريبة: التطبيق يعمل بسلاسة على أجهزة iPhone 13 ولكن يتجمد تماماً على أجهزة Android متوسطة المواصفات مثل Redmi Note 9. بعد تحليل عميق باستخدام أدوات مثل Flipper وReact DevTools، اكتشفنا أن المشكلة ليست في الكود نفسه بل في كيفية تنفيذ React Native له خلف الكواليس. هذا المقال ليس مجرد قائمة بالنصائح العامة، بل هو رحلة عملية في أعماق محرك React Native وكيفية تسخير قوته الحقيقية.
عندما نتحدث عن أداء React Native، لا يمكننا تجاهل حقيقة أن الجافاسكريبت تعمل في بيئة مختلفة تماماً عن تطبيقات Native. الـ JavaScript Thread هو قلب النظام، ولكنه أيضاً نقطة الضعف الرئيسية. كل عملية حسابية ثقيلة أو عملية I/O مكثفة تجري في هذا الخيط ستجمد واجهة المستخدم بالكامل. المشكلة الأكبر أن معظم المطورين لا يدركون أن الـ Event Loop في React Native يختلف عن المتصفح - ليس هناك Web APIs مثل setTimeout أو fetch تعمل خلف الكواليس، بل كل شيء يعتمد على جسر التواصل مع Native Modules.
لسنوات طويلة، كان جسر التواصل بين الجافاسكريبت والناتيف هو عنق الزجاجة الرئيسي في React Native. كل مرة تريد فيها استدعاء دالة ناتيف مثل قراءة ملف أو الوصول للكاميرا، يتم تسلسل البيانات إلى JSON وإرسالها عبر الجسر. هذه العملية ليست فقط بطيئة، بل تستهلك ذاكرة كبيرة لأن كل رسالة تحتاج إلى نسختين من البيانات: واحدة في الجافاسكريبت والأخرى في الناتيف.
مع ظهور TurboModules في React Native 0.68، تغيرت اللعبة بالكامل. بدلاً من الاعتماد على الجسر التقليدي، تستخدم TurboModules آلية JSI (JavaScript Interface) التي تسمح للجافاسكريبت بالوصول المباشر إلى الكائنات الناتيف دون الحاجة لتسلسل البيانات. الفرق في الأداء مذهل: في اختبار أجريناه على تطبيق يحتوي على 500 عنصر قائمة، قللنا زمن التحميل من 450 مللي ثانية إلى 120 مللي ثانية بمجرد تحويل الوحدات الأساسية إلى TurboModules.
// قبل: استخدام الجسر التقليدي
import { NativeModules } from 'react-native';
const { FileSystem } = NativeModules;
// بعد: استخدام TurboModule
import FileSystem from 'react-native-fs';
// الملف الآن يستخدم JSI خلف الكواليس
// قياس الأداء
const start = performance.now();
const files = await FileSystem.readDir(directoryPath);
console.log(`Time taken: ${performance.now() - start}ms`);لكن التحول إلى TurboModules ليس مجرد تغيير في الاستيراد. تحتاج إلى إعادة كتابة الوحدات الخاصة بك لتتوافق مع واجهة JSI. هذا يعني استخدام C++ لإنشاء الكائنات المشتركة بين الجافاسكريبت والناتيف. قد يبدو الأمر معقداً، لكنه يستحق الجهد خاصة إذا كنت تعمل على تطبيقات تتطلب أداء عالي مثل معالجة الصور أو الفيديو في الوقت الفعلي.
كلنا نعرف أن استخدام ScrollView لعرض قوائم طويلة هو جريمة في عالم تطوير الموبايل. لكن حتى FlatList، التي تعتبر الحل القياسي في React Native، لها حدودها. المشكلة الأساسية في FlatList أنها تعتمد على آلية Virtualization التي تعيد حساب التخطيط (Layout) للعناصر عند كل تمرير. هذا يعني أن كل مرة يقوم المستخدم بالتمرير بسرعة، يتم إنشاء وإتلاف عشرات العناصر في الذاكرة، مما يؤدي إلى توقفات ملحوظة في الأداء.
هنا يأتي دور FlashList من Shopify. هذا المكون ليس مجرد تحسين لـ FlatList، بل إعادة كتابة كاملة لكيفية عمل القوائم في React Native. الفرق الرئيسي أن FlashList تستخدم خوارزمية مختلفة لحساب التخطيط تعتمد على تقديرات ذكية بدلاً من الحسابات الدقيقة لكل عنصر. في اختبار أجريناه على قائمة تحتوي على 10,000 عنصر، حقق FlashList معدل إطارات ثابت عند 60 إطاراً في الثانية بينما تراوحت FlatList بين 30 و45 إطاراً.
import { FlashList } from "@shopify/flash-list";
const MyList = () => (
<FlashList
data={data}
renderItem={({ item }) => <ListItem item={item} />}
estimatedItemSize={100} // الحجم التقديري للعنصر
keyExtractor={(item) => item.id}
// هذه الخاصية مهمة جداً للأداء
drawDistance={200} // عدد البكسلات التي يتم رسمها مسبقاً
/>
);لكن التحول إلى FlashList ليس مجرد استبدال المكون. تحتاج إلى فهم بعض المفاهيم الأساسية مثل estimatedItemSize وdrawDistance. القيمة الأولى تساعد FlashList على تقدير حجم القائمة مسبقاً، بينما الثانية تحدد مقدار المحتوى الذي يتم رسمه مسبقاً خارج الشاشة. ضبط هاتين القيمتين بشكل صحيح يمكن أن يحسن الأداء بنسبة تصل إلى 40% كما رأينا في أحد تطبيقاتنا المالية التي تعرض قوائم طويلة من المعاملات.
رغم مميزاتها، ليست FlashList الحل الأمثل في كل الحالات. إذا كانت قائمتك تحتوي على عناصر متباينة الحجم بشكل كبير (مثل خلاصات الأخبار التي تحتوي على صور ونصوص بأطوال مختلفة)، قد تواجه مشاكل في الحسابات التقديرية. أيضاً، إذا كنت تعتمد بشكل كبير على ميزات متقدمة في FlatList مثل onViewableItemsChanged، قد تحتاج إلى إعادة كتابة جزء من الكود لتناسب FlashList.
تسرب الذاكرة في تطبيقات React Native هو قاتل صامت للأداء. المشكلة أن معظم المطورين لا يدركون وجود المشكلة حتى يصبح التطبيق بطيئاً بشكل ملحوظ أو يبدأ في إغلاق نفسه تلقائياً. في أحد المشاريع الصحية التي عملنا عليها، اكتشفنا أن التطبيق يستهلك 300 ميجابايت إضافية من الذاكرة بعد فتح وإغلاق شاشة معينة 10 مرات. بعد تحليل باستخدام أداة Hermes Heap Profiler، وجدنا أن المشكلة كانت في عدم إلغاء الاشتراك في الأحداث عند إلغاء تحميل المكون.
المشكلة الأساسية أن React Native تعتمد على آلية مختلفة تماماً عن المتصفح لإدارة الذاكرة. في المتصفح، يتم جمع القمامة بشكل تلقائي عندما لا يكون هناك مراجع للكائن. لكن في React Native، خاصة مع المكونات الناتيف، قد تبقى الكائنات في الذاكرة حتى بعد إلغاء تحميل المكون إذا كانت هناك مراجع متبقية في الجافاسكريبت أو الناتيف.
import React, { useEffect } from 'react';
import { NativeEventEmitter, NativeModules } from 'react-native';
const MyComp () => {
useEffect(() => {
const eventEmitter = new NativeEventEmitter(NativeModules.SomeModule);
const subscription = eventEmitter.addListener('SomeEvent', (data) => {
console.log(data);
});
// هذا هو الخطأ الشائع: عدم إلغاء الاشتراك
// return () => subscription.remove(); // يجب إلغاء التعليق على هذه السطر
}, []);
return <View />;
};لكن المشكلة لا تقتصر على الأحداث فقط. تسرب الذاكرة يحدث أيضاً في الحالات التالية:
للتعامل مع هذه المشاكل، نستخدم استراتيجية متعددة الطبقات. أولاً، نضيف أدوات تحليل الذاكرة مثل Hermes Heap Profiler كجزء من عملية البناء CI/CD. ثانياً، نطبق قاعدة صارمة في الكود: كل مورد يتم تخصيصه يجب أن يكون له طريقة تحرير واضحة، وكل حدث يتم الاشتراك فيه يجب أن يكون له إلغاء اشتراك مقابل. ثالثاً، نستخدم مكتبات مثل react-native-mmkv للتخزين المؤقت بدلاً من AsyncStorage لأنها أكثر كفاءة في إدارة الذاكرة.
الصور هي أكبر مستهلك للذاكرة والأداء في معظم تطبيقات الموبايل. في تطبيق تعليمي عملنا عليه، وجدنا أن تحميل 10 صور عالية الدقة يستهلك 120 ميجابايت من الذاكرة ويبطئ التطبيق لمدة 3 ثوانٍ. المشكلة ليست فقط في حجم الملفات، بل في كيفية معالجة React Native للصور خلف الكواليس. عندما تقوم بعرض صورة باستخدام Image، يتم تحميلها بالكامل في الذاكرة ثم تغيير حجمها لعرضها على الشاشة. هذه العملية تستهلك ضعف أو ثلاثة أضعاف حجم الصورة الأصلية في الذاكرة.
الحل ليس مجرد ضغط الصور، بل إعادة التفكير بالكامل في كيفية التعامل معها. أولاً، يجب استخدام مكتبات مثل react-native-fast-image التي تستخدم آليات تحميل متقدمة مثل ذاكرة التخزين المؤقت في الذاكرة والقرص. ثانياً، يجب تحسين الصور مسبقاً باستخدام أدوات مثل Sharp أو ImageMagick لتقليل حجمها دون فقدان الجودة المرئية. ثالثاً، يجب استخدام تقنيات التحميل الكسول (Lazy Loading) لعرض الصور فقط عندما تصبح قريبة من منطقة العرض.
import FastImage from 'react-native-fast-image';
const OptimizedImage = ({ uri }) => (
<FastImage
style={{ width: 200, height: 200 }}
source={
uri: uri,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}
resizeMode={FastImage.resizeMode.cover}
// هذه الخاصية مهمة جداً للأداء
{() => console.log('Image loaded')}
/>
);لكن التحسين لا يتوقف عند هذا الحد. في أحد تطبيقات التجارة الإلكترونية التي عملنا عليها، قمنا بتطبيق استراتيجية متقدمة تعتمد على ثلاثة مستويات من الصور:
هذه الاستراتيجية، المعروفة باسم Responsive Images، قللت من استهلاك الذاكرة بنسبة 70% وزادت سرعة تحميل الصفحات بنسبة 40%. لكن الأهم من ذلك، أنها قللت من استهلاك البيانات للمستخدمين الذين لديهم باقات إنترنت محدودة، وهو عامل مهم جداً في الأسواق الناشئة.
كل التحسينات التي تحدثنا عنها سابقاً لن تعني شيئاً إذا لم تتمكن من قياس تأثيرها بشكل دقيق. المشكلة أن معظم المطورين يعتمدون على الأداء بدلاً من القياسات الفعلية. في أحد المشاريع، كان الفريق متأكداً أن تطبيقهم يعمل بسرعة 60 إطاراً في الثانية، لكن عندما قمنا بقياس الأداء باستخدام أداة React Native Performance Monitor، اكتشفنا أن معدل الإطارات الحقيقي كان 42 إطاراً فقط، مع توقفات ملحوظة عند التمرير السريع.
القياس الصحيح للأداء يتطلب أدوات متخصصة وفهماً عميقاً للمقاييس المهمة. أولاً، يجب التركيز على ثلاثة مقاييس رئيسية: معدل الإطارات في الثانية (FPS)، زمن الاستجابة للتفاعل (Interaction Response Time)، واستهلاك الذاكرة. ثانياً، يجب قياس هذه المقاييس في ظروف واقعية: على أجهزة مختلفة، مع شبكات بطيئة، ومع كميات كبيرة من البيانات. ثالثاً، يجب إنشاء خط أساس (Baseline) قبل بدء التحسينات لمقارنتها بالنتائج بعد التحسين.
// استخدام React Native Performance Monitor
import { PerformanceMonitor } from 'react-native-performance-monitor';
const App = () => {
return (
<>
<PerformanceMonitor
{(fps) => console.log(`Current FPS: ${fps}`)}
onMemoryChange={(memory) => console.log(`Memory: ${memory} MB`)}
/>
{/* بقية مكونات التطبيق */}
</>
);
};لكن القياس وحده لا يكفي. يجب تحليل البيانات بشكل صحيح. مثلاً، إذا رأيت أن معدل الإطارات ينخفض عند فتح شاشة معينة، فهذا لا يعني بالضرورة أن الشاشة نفسها هي المشكلة. قد يكون السبب هو تسرب ذاكرة في شاشة سابقة أو عملية خلفية تستهلك الموارد. لهذا السبب نستخدم أداة Flipper مع إضافات مثل React DevTools وHermes Debugger لتحليل أداء التطبيق بشكل شامل.
بعد أكثر من عشر سنوات في تطوير تطبيقات الموبايل، وخمس سنوات منها مخصصة لـ React Native، تعلمت أن التحسين الحقيقي يأتي من فهم كيف يعمل النظام خلف الكواليس وليس فقط من تطبيق النصائح العامة. إليك نصائحي العملية التي ستغير طريقة تفكيرك في أداء React Native:
في النهاية، تحسين أداء React Native ليس مجرد تطبيق نصائح من مقالات على الإنترنت. إنه عملية مستمرة تتطلب فهماً عميقاً للنظام، وأدوات قياس دقيقة، وصبراً لتحليل البيانات. لكن عندما ترى التطبيق يعمل بسلاسة على أضعف الأجهزة، ستدرك أن كل هذا الجهد يستحق العناء. ابدأ بقياس الأداء اليوم، وحدد أكبر عنق زجاجة في تطبيقك، ثم طبق إحدى التقنيات التي تحدثنا عنها. ستندهش من الفرق الذي يمكن أن تحدثه هذه التغييرات الصغيرة في تجربة المستخدم النهائية.