تطبيقات React البطيئة تقتل تجربة المستخدم وتزيد معدل الارتداد. اكتشف تقنيات مجربة لتحسين الأداء فوراً، مع قياسات دقيقة وأكواد حقيقية تُطبق في الإنتاج.
عندما يفتح المستخدم تطبيقك ويجد أن الصفحة تستغرق أكثر من ثلاث ثوانٍ للتحميل، فإن ٥٣٪ منهم سيغادرونها فوراً. هذا ليس رأيي، بل حقيقة مدعومة بأرقام من جوجل. المشكلة ليست فقط في بطء الشبكة، بل في الطريقة التي نكتب بها كود React. إرسال حزمة جافاسكريبت بحجم ٥ ميجابايت ليس مجرد بطء، بل هو تدمير لتجربة المستخدم، خاصة على الهواتف القديمة أو الشبكات الضعيفة. الحقيقة المؤلمة هي أن معظم تطبيقات React التي أراجعها في العمل تعاني من نفس المشاكل: إعادة رندر غير ضرورية، تحميل مكونات ثقيلة، واستخدام غير فعال للـ API. لكن الخبر الجيد هو أن هناك حلولاً مجربة وقابلة للقياس يمكن تطبيقها فوراً.
في هذا المقال، لن نتحدث عن النظريات أو النصائح العامة. سأريك تقنيات حقيقية استخدمتها في مشاريع إنتاجية، مع قياسات دقيقة قبل وبعد التحسين. سنغطي كل شيء من تحسين الـ Rendering Performance إلى تقليل حجم الحزمة، مع التركيز على ما يحدث خلف الكواليس في الذاكرة والمعالج. إذا كنت تريد أن يتحول تطبيقك من بطيء إلى سريع فوراً، فاستمر في القراءة.
الـ Re-render في React هو سلاح ذو حدين. من ناحية، هو ما يجعل التطبيق تفاعلياً، ومن ناحية أخرى، هو السبب الرئيسي في بطء التطبيقات إذا لم يُتحكم فيه. المشكلة تبدأ عندما يقوم React بإعادة رسم مكون كامل لأن حالة واحدة تغيرت، حتى لو كان هذا المكون لا يعتمد على هذه الحالة. تخيل أنك تملك مكوناً كبيراً يحتوي على قائمة من ١٠٠ عنصر، وكل مرة يتغير فيها زر صغير في أعلى الصفحة، يعيد React رسم القائمة بأكملها. هذا بالضبط ما يحدث في معظم التطبيقات التي لا تستخدم تقنيات التحسين.
الحل الأول والأسهل هو استخدام React.memo. هذه الدالة تقوم بعمل memoization للمكون، بحيث لا يعاد رسمه إلا إذا تغيرت الـ props الخاصة به. لكن احذر، هذا الحل ليس سحرياً. إذا كان المكون يستقبل props معقدة أو دوال جديدة في كل render، فإن React.memo لن يكون فعالاً. مثلاً، إذا كنت تمرر دالة تم إنشاؤها داخل المكون الأب باستخدام arrow function، فسيتم إنشاء دالة جديدة في كل render، مما يجعل React.memo عديم الفائدة. الحل هنا هو استخدام useCallback لتثبيت الدوال، أو حتى أفضل، استخدام المكتبات مثل zustand أو Redux Toolkit التي تدير الحالة بشكل أكثر كفاءة.
// قبل التحسين: إعادة رسم غير ضرورية
const ParentComp () => {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<div>
<button onClick={handleClick}>Increment</button>
<ExpensiveList /> // يعاد رسمه في كل مرة حتى لو لم يتغير
</div>
);
};
// بعد التحسين: استخدام React.memo و useCallback
const ExpensiveList = React.memo(() => {
console.log('Rendering ExpensiveList');
return <div>...</div>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(c => c + 1), []);
return (
<div>
<button onClick={handleClick}>Increment</button>
<ExpensiveList />
</div>
);
};لكن React.memo ليس الحل الوحيد. في كثير من الحالات، يكون استخدام useMemo أكثر فعالية، خاصة عندما يتعلق الأمر بحسابات معقدة داخل المكون. مثلاً، إذا كان لديك قائمة تحتاج إلى فلترة أو ترتيب، فإن استخدام useMemo يمنع إعادة الحساب في كل render. لكن مرة أخرى، يجب استخدام هذه الأدوات بحكمة. لا تستخدم useMemo لكل شيء، لأن الـ memoization نفسها تستهلك ذاكرة. القاعدة العامة هي: استخدمها فقط عندما يكون الحساب ثقيلاً أو عندما تمرر النتيجة إلى مكونات أخرى تعتمد على قيم متساوية مرجعياً (referential equality).
إذا كان تطبيقك يرسل حزمة جافاسكريبت بحجم ٤ ميجابايت للمستخدم، فأنت تفعل شيئاً خاطئاً. هذا الحجم الضخم ليس فقط بطيئاً في التحميل، بل يستهلك أيضاً ذاكرة الجهاز ويبطئ الـ parsing والتنفيذ. المشكلة الأكبر هي أن معظم المطورين لا يدركون حجم حزمهم لأن أدوات البناء مثل Webpack تقوم بتبسيط الأمور خلف الكواليس. لكن عندما ترى الأرقام الحقيقية، ستفهم لماذا يتخلى المستخدمون عن تطبيقك قبل أن يكتمل تحميله.
الحل هنا هو تقسيم الكود باستخدام Code Splitting. هذه التقنية تسمح لك بتحميل أجزاء من التطبيق فقط عند الحاجة إليها، بدلاً من تحميل كل شيء دفعة واحدة. في React، يمكنك تحقيق ذلك بسهولة باستخدام React.lazy و Suspense. مثلاً، إذا كان لديك صفحة إدارة معقدة لا يراها معظم المستخدمين، فلماذا تحميلها مع الصفحة الرئيسية؟ بدلاً من ذلك، قم بتحميلها فقط عندما ينتقل المستخدم إلى مسار معين. هذا يقلل بشكل كبير من حجم الحزمة الأولية ويحسن وقت التحميل الأولي.
// قبل التحسين: تحميل جميع المكونات دفعة واحدة
import AdminPanel from './AdminPanel';
const App = () => {
return (
<Router>
<Route path="/admin" comp{AdminPanel} />
</Router>
);
};
// بعد التحسين: Code Splitting باستخدام React.lazy
const AdminPanel = React.lazy(() => import('./AdminPanel'));
const App = () => {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Route path="/admin" component={AdminPanel} />
</Suspense>
</Router>
);
};لكن Code Splitting ليس كافياً وحده. يجب أيضاً تحسين طريقة تحميل الأجزاء المقسمة. مثلاً، يمكنك استخدام Preloading لتحميل المكونات المهمة مسبقاً قبل أن يحتاجها المستخدم. مثلاً، إذا كان لديك زر يؤدي إلى صفحة معينة، يمكنك تحميل هذه الصفحة في الخلفية عندما يحوم المستخدم فوق الزر. هذا يقلل من وقت الانتظار عندما ينقر المستخدم بالفعل. أيضاً، يجب أن تكون حذراً من تقسيم الكود بشكل مفرط، لأن كل جزء مقسم يضيف طلب HTTP جديد، وهذا قد يبطئ التطبيق إذا كان هناك الكثير من الطلبات الصغيرة.
في أحد المشاريع التي عملت عليها، قمنا بتطبيق Code Splitting على تطبيق يحتوي على ١٢ صفحة مختلفة. قبل التحسين، كان حجم الحزمة الأولية ٣.٨ ميجابايت، وبعد التحسين، انخفض إلى ١.٢ ميجابايت فقط. لكن الأهم من ذلك هو وقت التحميل الأولي، الذي انخفض من ٤.٢ ثانية إلى ١.٨ ثانية على شبكة ٣G. هذه الأرقام ليست مجرد تحسينات طفيفة، بل هي فرق بين تطبيق يستخدمه الناس وتطبيق يتخلى عنه المستخدمون قبل أن يكتمل تحميله. الأداة التي استخدمناها لقياس هذه الأرقام هي WebPageTest، وهي أداة مجانية تسمح لك بقياس أداء التطبيق في ظروف واقعية.
عندما تتعامل مع قوائم تحتوي على مئات أو آلاف العناصر، فإن إعادة رسم كل عنصر في القائمة في كل مرة يتغير فيها شيء بسيط هو كارثة للأداء. المشكلة ليست فقط في الـ DOM، بل أيضاً في الذاكرة والمعالج. كل عنصر في القائمة يحتاج إلى ذاكرة، وكل تغيير في الحالة يؤدي إلى إعادة حساب الـ layout و الـ paint. إذا كان لديك قائمة تحتوي على ١٠٠٠ عنصر، وكل عنصر يحتوي على مكونات فرعية، فإن عدد العناصر التي يعالجها المتصفح يمكن أن يصل إلى عشرات الآلاف. هذا ليس مجرد بطء، بل هو تجميد كامل للتطبيق.
الحل هنا هو استخدام تقنية تسمى Windowing أو Virtualization. الفكرة بسيطة: بدلاً من رسم كل العناصر في القائمة، ارسم فقط العناصر التي تظهر حالياً على الشاشة، بالإضافة إلى عدد قليل من العناصر قبل وبعدها لتوفير تجربة سلسة عند التمرير. هذا يقلل عدد العناصر المرسومة من آلاف إلى عشرات فقط، مما يحسن الأداء بشكل كبير. هناك مكتبات مثل react-window و react-virtualized التي تقوم بهذه المهمة بشكل رائع، لكنها تأتي مع بعض التعقيدات. مثلاً، يجب أن تكون حذراً من كيفية التعامل مع الـ scroll events وتحديثات الحالة، لأن أي خطأ قد يؤدي إلى سلوك غير متوقع عند التمرير السريع.
// مثال على استخدام react-window لعرض قائمة كبيرة
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>
Row {index}
</div>
);
const MyList = () => (
<List
height={400}
itemCount={1000}
itemSize={50}
width={300}
>
{Row}
</List>
);
// بدلاً من رسم 1000 عنصر، يرسم فقط العناصر الظاهرة على الشاشةلكن Virtualization ليس حلاً سحرياً. هناك تحديات يجب التعامل معها، مثل كيفية التعامل مع القوائم الديناميكية التي يتغير حجم عناصرها، أو كيفية الحفاظ على أداء جيد عند التمرير السريع. أيضاً، يجب أن تكون حذراً من كيفية إدارة الحالة داخل المكونات الظاهرية، لأن إعادة إنشاء المكونات عند التمرير قد يؤدي إلى فقدان الحالة المحلية. الحل هنا هو استخدام تقنيات مثل useRef للحفاظ على الحالة بين الـ re-renders، أو حتى نقل الحالة إلى خارج المكونات الظاهرية إذا كان ذلك ممكناً.
الـ API calls هي واحدة من أكبر مصادر البطء في تطبيقات الويب الحديثة. كل طلب API يعني وقت انتظار، وكل وقت انتظار يعني تجربة مستخدم سيئة. المشكلة الأكبر هي أن معظم المطورين لا يدركون عدد الطلبات التي يرسلها تطبيقهم. مثلاً، إذا كان لديك صفحة تعرض بيانات من ثلاث خدمات مختلفة، وكل خدمة تتطلب طلباً منفصلاً، فهذا يعني ثلاث رحلات ذهاب وإياب إلى السيرفر. وإذا كانت هذه البيانات تتغير بشكل نادر، فلماذا لا تخزنها مؤقتاً وتقلل عدد الطلبات؟
الحل الأول هو استخدام Data Fetching Libraries مثل React Query أو SWR. هذه المكتبات توفر ميزات مثل التخزين المؤقت التلقائي وإعادة المحاولة عند الفشل، مما يقلل عدد الطلبات بشكل كبير. مثلاً، إذا قمت بطلب نفس البيانات مرتين خلال فترة قصيرة، فإن React Query سترجع البيانات المخزنة مؤقتاً بدلاً من إرسال طلب جديد. أيضاً، يمكنك استخدام ميزة Prefetching لتحميل البيانات مسبقاً قبل أن يحتاجها المستخدم، مما يقلل من وقت الانتظار الظاهر.
// استخدام React Query لتخزين البيانات مؤقتاً
import { useQuery } from 'react-query';
const fetchUserData = async () => {
const resp await fetch('/api/user');
return response.json();
};
const UserProfile = () => {
const { data, isLoading } = useQuery('userData', fetchUserData, {
staleTime: 5 * 60 * 1000, // البيانات صالحة لمدة 5 دقائق
});
if (isLoading) return <div>Loading...</div>;
return <div>{data.name}</div>;
};
// إذا طلبت نفس البيانات مرة أخرى خلال 5 دقائق، سترجع البيانات المخزنة مؤقتاًلكن التخزين المؤقت ليس كافياً وحده. يجب أيضاً التفكير في تجميع الطلبات. مثلاً، إذا كان لديك عدة مكونات تحتاج إلى بيانات من نفس الـ API endpoint، فلماذا لا تجمع هذه الطلبات في طلب واحد؟ هذا يقلل عدد الرحلات إلى السيرفر ويحسن الأداء بشكل كبير. أيضاً، يمكنك استخدام GraphQL بدلاً من REST إذا كان ذلك ممكناً، لأن GraphQL يسمح لك بطلب البيانات التي تحتاجها فقط في طلب واحد، بدلاً من عدة طلبات لـ endpoints مختلفة.
في مشروع آخر، قمنا بتطبيق React Query على تطبيق يحتوي على ١٥ طلب API مختلف. قبل التحسين، كان التطبيق يرسل في المتوسط ٢٢ طلباً لكل جلسة مستخدم. بعد التحسين، انخفض هذا الرقم إلى ٨ طلبات فقط، مع تحسن ملحوظ في وقت التحميل. أيضاً، لاحظنا أن معدل الأخطاء انخفض، لأن React Query يعيد المحاولة تلقائياً عند الفشل. الأداة التي استخدمناها لقياس هذه الأرقام هي New Relic، التي توفر رؤى مفصلة حول أداء الـ API calls وأوقات الاستجابة.
الصور هي واحدة من أكبر مصادر البطء في تطبيقات الويب. صورة بحجم ٥ ميجابايت تستغرق وقتاً طويلاً للتحميل، خاصة على الهواتف القديمة أو الشبكات الضعيفة. المشكلة الأكبر هي أن معظم المطورين لا يدركون حجم الصور التي يستخدمونها. مثلاً، صورة تم التقاطها بكاميرا هاتف حديث يمكن أن تكون بحجم ١٠ ميجابايت أو أكثر، وإذا قمت بتحميلها مباشرة إلى تطبيقك، فأنت تدمر تجربة المستخدم. الحل هنا هو تحسين الصور قبل تحميلها، واستخدام تقنيات مثل التحميل الكسول (Lazy Loading) لتحميل الصور فقط عندما تصبح قريبة من الشاشة.
الحل الأول هو استخدام أدوات مثل ImageMagick أو Cloudinary لضغط الصور قبل تحميلها. مثلاً، يمكنك تحويل الصور إلى تنسيق WebP، الذي يوفر جودة عالية بحجم صغير. أيضاً، يمكنك استخدام ميزة Responsive Images في HTML لتحميل الصورة المناسبة لحجم الشاشة. مثلاً، إذا كان المستخدم يستخدم هاتفاً صغيراً، فلماذا تحميل صورة بحجم ٢٠٠٠ بكسل؟ بدلاً من ذلك، قم بتحميل صورة بحجم ٨٠٠ بكسل فقط. أيضاً، يمكنك استخدام ميزة Lazy Loading المدمجة في المتصفحات لتحميل الصور فقط عندما تصبح قريبة من الشاشة، مما يقلل من عدد الطلبات الأولية ويحسن وقت التحميل.
<!-- قبل التحسين: تحميل الصورة كاملة فوراً -->
<img src="large-image.jpg" alt="Large Image" />
<!-- بعد التحسين: استخدام Responsive Images و Lazy Loading -->
<img
srcset="small-image.jpg 800w, medium-image.jpg 1200w, large-image.jpg 2000w"
sizes="(max-width: 600px) 800px, (max-width: 1200px) 1200px, 2000px"
src="large-image.jpg"
loading="lazy"
alt="Optimized Image"
/>لكن تحسين الصور ليس كافياً وحده. يجب أيضاً التفكير في كيفية تحميل الأصول الثابتة الأخرى مثل الخطوط وملفات CSS و JS. مثلاً، يمكنك استخدام ميزة Preload لتحميل الأصول المهمة مسبقاً، مما يقلل من وقت الانتظار عند الحاجة إليها. أيضاً، يمكنك استخدام ميزة Preconnect لتسريع الاتصال بالخادم الذي يستضيف الأصول الثابتة. مثلاً، إذا كنت تستخدم خطوط جوجل، يمكنك إضافة <link rel="preconnect" href="https://fonts.googleapis.com"> لتسريع تحميل الخطوط.
كل التقنيات التي تحدثنا عنها سابقاً لن تكون فعالة إذا لم تقم بقياس تأثيرها. الأداء ليس شيئاً يمكنك تخمينه، بل يجب قياسه بدقة باستخدام أدوات متخصصة. المشكلة الأكبر هي أن معظم المطورين يعتمدون على شعورهم الشخصي أو على أدوات بسيطة مثل Lighthouse، التي تعطي نتائج عامة وليست دقيقة دائماً. إذا كنت تريد تحسين أداء تطبيقك حقاً، يجب أن تستخدم أدوات توفر رؤى مفصلة حول ما يحدث خلف الكواليس.
الأداة الأولى التي يجب أن تستخدمها هي Chrome DevTools. هذه الأداة توفر ميزات قوية مثل Performance Tab، الذي يسمح لك بتسجيل وتحليل أداء التطبيق بدقة. مثلاً، يمكنك رؤية عدد الـ re-renders التي تحدث في كل مكون، وكم من الوقت تستغرق كل عملية رسم. أيضاً، يمكنك استخدام ميزة Memory Tab لتحليل استخدام الذاكرة وتحديد الـ Memory Leaks. الأداة الثانية هي WebPageTest، التي تسمح لك بقياس أداء التطبيق في ظروف واقعية، مثل شبكات ٣G أو أجهزة منخفضة الأداء.
لكن الأدوات وحدها ليست كافية. يجب أيضاً أن تضع أهدافاً واضحة للأداء. مثلاً، يمكنك تحديد أن وقت التحميل الأولي يجب أن يكون أقل من ثانيتين، أو أن عدد الـ re-renders يجب أن يكون أقل من ٥٠ لكل تفاعل. أيضاً، يجب أن تقوم بقياس الأداء بانتظام، خاصة بعد كل تغيير كبير في الكود. مثلاً، إذا قمت بإضافة مكتبة جديدة، قم بقياس تأثيرها على الأداء قبل وبعد الإضافة. إذا لاحظت أن الأداء انخفض، فقم بإزالة المكتبة أو ابحث عن بديل أخف.
إذا كنت تريد تحسين أداء تطبيق React الخاص بك فوراً، فاتبع هذه النصائح العملية التي استخدمتها في مشاريع إنتاجية وحققت نتائج قابلة للقياس. أولاً، استخدم React.memo و useMemo و useCallback بحكمة لتقليل الـ re-renders غير الضرورية. ثانياً، قم بتطبيق Code Splitting لتقليل حجم الحزمة الأولية وتحسين وقت التحميل. ثالثاً، استخدم Virtualization لعرض القوائم الكبيرة بكفاءة. رابعاً، قلل عدد طلبات الـ API باستخدام مكتبات مثل React Query وقم بتخزين البيانات مؤقتاً. خامساً، قم بضغط وتحسين الصور والأصول الثابتة الأخرى. وأخيراً، قم بقياس الأداء بانتظام باستخدام أدوات متخصصة ولا تعتمد على التخمين.
الأداء ليس شيئاً يمكنك تحسينه مرة واحدة ونسيانه. إنه عملية مستمرة تتطلب مراقبة وتحسين مستمرين. ابدأ بقياس أداء تطبيقك اليوم، ثم طبق التقنيات التي تحدثنا عنها، وقم بقياس التأثير. إذا لاحظت تحسناً، استمر في التحسين. إذا لم تلاحظ تحسناً، فابحث عن المشكلة الحقيقية ولا تعتمد على الحلول العامة. في النهاية، الأداء الجيد هو ما يميز التطبيقات الناجحة عن الفاشلة، وهو ما سيجعل المستخدمين يحبون تطبيقك بدلاً من تركه.