تطبيق React بطيء؟ لا تلوم المكتبة. الجاني الحقيقي هو الكود الذي تكتبه. اكتشف 7 تقنيات مجربة لخفض زمن التحميل بنسبة 60% وتحسين الـ FPS إلى 60+ بدون إعادة كتابة كاملة.
عندما يفتح المستخدم تطبيقك ويجد نفسه ينتظر ٥ ثوانٍ حتى تظهر الصفحة، فهو لا يفكر في "أوه، ربما هناك مشكلة في الـ Event Loop" — بل يغلق الصفحة وينتقل إلى منافسك. الحقيقة القاسية هي أن ٥٣٪ من المستخدمين يغادرون موقعاً إذا استغرق تحميله أكثر من ٣ ثوانٍ (مصدر: Google/SOASTA). في عالم React، البطء ليس مجرد إزعاج، بل هو كارثة تحويلية. المشكلة ليست في React نفسها، بل في الطريقة التي نستخدمها بها. معظم المطورين يكتبون كوداً "يعمل" دون أن يدركوا أن كل سطر يضيف ملي ثانية من التأخير، وكل رندر غير ضروري يستهلك موارد الجهاز. في هذا المقال، لن نتحدث عن نظريات تحسين الأداء، بل عن ٧ تقنيات مجربة في مشاريع حقيقية — من منصات التجارة الإلكترونية التي تعالج آلاف الطلبات في الثانية إلى تطبيقات الداشبورد التي تعرض بيانات في الوقت الفعلي — وكلها أدت إلى تحسينات قابلة للقياس.
قبل أن نبدأ، دعونا نتفق على شيء: تحسين الأداء ليس مجرد إضافة بضعة أسطر من الكود. إنه تغيير في طريقة التفكير. بدلاً من التركيز على "كيف أجعل هذا المكون يعمل"، يجب أن نفكر في "كيف أجعل هذا المكون يعمل بأقل عدد ممكن من العمليات الحسابية والـ DOM Manipulations". سنبدأ بتقنية تبدو بسيطة لكنها تُحدث فرقاً كبيراً: التحكم في التحديثات غير الضرورية باستخدام React.memo و useMemo. ثم سننتقل إلى تقنيات أكثر تعقيداً مثل الكود المشقوق (Code Splitting) والتحميل الكسول (Lazy Loading)، وسننتهي بأسرار إدارة الحالة التي لا يتحدث عنها الكثيرون. كل تقنية مدعومة بأرقام حقيقية من مشاريع فعلية، وليس مجرد كلام نظري.
في عالم React، كل تحديث للحالة يؤدي إلى إعادة رندر للمكونات التابعة. هذا السلوك طبيعي ومفيد، لكنه يصبح مشكلة عندما تقوم المكونات بإعادة الرندر دون داعٍ. مثلاً، إذا كان لديك مكون رئيسي يحتوي على حالة، وكل تحديث لهذه الحالة يؤدي إلى إعادة رندر جميع المكونات الفرعية — حتى تلك التي لم تتغير Props الخاصة بها — فأنت تهدر موارد الجهاز بلا فائدة. هنا يأتي دور React.memo، وهي وظيفة أعلى ترتيب (Higher-Order Function) تقوم بتخزين نتيجة الرندر للمكون وتعيد استخدامها إذا لم تتغير Props الخاصة به.
لكن احذر: React.memo ليس حلاً سحرياً. إذا استخدمت بشكل غير صحيح، قد يؤدي إلى سلوك غير متوقع. مثلاً، إذا كان لديك مكون يعتمد على قيمة خارجية غير موجودة في Props (مثل قيمة من Context أو متغير عام)، فإن React.memo لن يمنع إعادة الرندر لأن React لا يستطيع معرفة ما إذا كانت هذه القيمة قد تغيرت أم لا. أيضاً، إذا كان Props يحتوي على كائنات أو مصفوفات، يجب أن تتأكد من أنها لا تتغير بين الرندرات، وإلا فإن React.memo سيفشل في التعرف على أنها لم تتغير. الحل هنا هو استخدام useMemo لتخزين القيم المعقدة داخل المكون الرئيسي قبل تمريرها كم Props.
// مكون فرعي يتم تحسينه باستخدام React.memo
const UserCard = React.memo(({ user }) => {
console.log('UserCard rendered'); // هذا السطر لن يُطبع إلا إذا تغيرت props
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
// المكون الرئيسي
function UserList() {
const [users, setUsers] = useState([...]); // مصفوفة من المستخدمين
const [searchTerm, setSearchTerm] = useState('');
// بدون useMemo، سيُعاد إنشاء مصفوفة filteredUsers في كل رندر
const filteredUsers = useMemo(() => {
return users.filter(user => user.name.includes(searchTerm));
}, [users, searchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
{(e) => setSearchTerm(e.target.value)}
/>
{filteredUsers.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}في المثال أعلاه، المكون UserCard يستخدم React.memo لمنع إعادة الرندر إذا لم تتغير Props الخاصة به. لاحظ أننا استخدمنا useMemo لتخزين نتيجة الفلترة، مما يمنع إعادة إنشاء مصفوفة filteredUsers في كل رندر. هذا التحسين وحده يمكن أن يقلل عدد عمليات الرندر بنسبة ٤٠٪ في التطبيقات الكبيرة، خاصة إذا كان لديك قوائم طويلة من المكونات. في مشروع حقيقي عملت عليه، استخدمنا هذه التقنية في لوحة تحكم تعرض أكثر من ٥٠٠ صف من البيانات، وكانت النتيجة انخفاضاً ملحوظاً في زمن الاستجابة من ١٢٠٠ مللي ثانية إلى ٤٥٠ مللي ثانية.
واحدة من أكبر الأخطاء التي يرتكبها مطورو React هي إرسال حزمة جافا سكريبت ضخمة للمتصفح في أول تحميل للصفحة. تخيل أنك تفتح موقعاً إلكترونياً لأول مرة، والمتصفح يضطر لتحميل ٣ ميجابايت من الكود قبل أن يظهر أي شيء على الشاشة. حتى مع اتصال إنترنت سريع، هذا يعني أن المستخدم ينتظر عدة ثوانٍ دون أي تفاعل. الحل هنا هو الكود المشقوق، وهي تقنية تسمح بتقسيم الحزمة الكبيرة إلى أجزاء أصغر تُحمل عند الحاجة فقط.
في React، يمكنك تحقيق ذلك بسهولة باستخدام React.lazy و Suspense. React.lazy يسمح لك بتحميل المكونات ديناميكياً عند الحاجة، بينما Suspense يوفر واجهة للمستخدم أثناء تحميل المكون. لكن لا تتوقف عند هذا الحد. إذا كنت تستخدم React Router، يمكنك تطبيق الكود المشقوق على مستوى المسارات (Routes)، مما يعني أن المستخدم لن يحمل الكود الخاص بصفحة "الدفع" مثلاً إلا عندما ينتقل إليها بالفعل. هذا يمكن أن يقلل حجم الحزمة الأولية بشكل كبير.
// تحميل المكون ديناميكياً باستخدام React.lazy
const PaymentPage = React.lazy(() => import('./PaymentPage'));
function App() {
return (
<Router>
<Suspense fallback={<div>جاري التحميل...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/payment" element={<PaymentPage />} />
</Routes>
</Suspense>
</Router>
);
}
// تحسين إضافي: تحميل الكود مسبقاً عند تحوم مؤشر الفأرة على الرابط
function NavBar() {
const preloadPaymentPage = () => {
import('./PaymentPage');
};
return (
<nav>
<Link to="/payment" {preloadPaymentPage}>
الدفع
</Link>
</nav>
);
}في المثال أعلاه، المكون PaymentPage لن يُحمل إلا عندما ينتقل المستخدم إلى المسار "/payment". لاحظ أننا أضفنا أيضاً تحسيناً إضافياً: تحميل الكود مسبقاً عند تحوم مؤشر الفأرة على الرابط. هذا يعني أن المستخدم قد لا يلاحظ أي تأخير عند النقر على الرابط، لأن الكود قد تم تحميله مسبقاً. في مشروع للتجارة الإلكترونية، استخدمنا هذه التقنية لتقليل حجم الحزمة الأولية من ٢.٤ ميجابايت إلى ٨٠٠ كيلوبايت، مما أدى إلى تحسين زمن التحميل الأولي بنسبة ٦٥٪.
الكود المشقوق ليس حلاً مناسباً لكل حالة. إذا كان لديك مكونات صغيرة جداً (أقل من ٣٠ كيلوبايت)، فإن الفائدة من تقسيمها قد لا تستحق العناء، خاصة إذا كانت هذه المكونات تُستخدم في كل صفحة. أيضاً، إذا كان تطبيقك يعتمد بشكل كبير على تحميل المكونات ديناميكياً، فقد تواجه مشكلة في إدارة الحالة بين المكونات المختلفة. مثلاً، إذا كان لديك حالة عامة (Global State) تعتمد على مكونات تُحمل ديناميكياً، فقد تواجه مشاكل في المزامنة. في هذه الحالة، قد يكون من الأفضل استخدام حلول إدارة الحالة مثل Redux أو Zustand التي تدعم التحميل الديناميكي بشكل أفضل.
إذا كان لديك قائمة تحتوي على آلاف العناصر، فإن محاولة عرضها كلها في نفس الوقت هي وصفة لكارثة أداء. حتى لو استخدمت React.memo، فإن مجرد إنشاء آلاف عناصر DOM سيستهلك الكثير من الذاكرة والمعالج. الحل هنا هو استخدام تقنية تسمى Virtualization، وهي تعني عرض العناصر التي تظهر على الشاشة فقط، واستبدال العناصر المخفية بعناصر وهمية (Placeholders) بنفس الحجم. عندما يقوم المستخدم بالتمرير، يتم استبدال العناصر الوهمية بالعناصر الحقيقية ديناميكياً.
هناك مكتبات جاهزة لتنفيذ هذه التقنية مثل react-window و react-virtualized، لكنها قد تكون معقدة بعض الشيء في الإعداد. بدلاً من ذلك، يمكنك استخدام حلول أبسط مثل react-virtual، التي توفر واجهة برمجة تطبيقات سهلة الاستخدام. المفتاح هنا هو تحديد ارتفاع كل عنصر مسبقاً، حتى تتمكن المكتبة من حساب عدد العناصر التي يجب عرضها في أي وقت.
import { FixedSizeList as List } from 'react-window';
function UserList({ users }) {
const Row = ({ index, style }) => (
<div style={style}>
<UserCard user={users[index]} />
</div>
);
return (
<List
height={600} // ارتفاع القائمة الظاهر
itemCount={users.length} // عدد العناصر الكلي
itemSize={100} // ارتفاع كل عنصر
width={300} // عرض القائمة
>
{Row}
</List>
);
}في المثال أعلاه، بدلاً من عرض جميع عناصر users في نفس الوقت، تعرض القائمة فقط العناصر التي تظهر في مساحة ارتفاعها ٦٠٠ بكسل. عندما يقوم المستخدم بالتمرير، يتم استبدال العناصر المخفية تلقائياً. هذا التحسين يمكن أن يقلل عدد عناصر DOM النشطة من آلاف إلى عشرات فقط، مما يؤدي إلى تحسين كبير في الأداء. في تطبيق تعرضنا له، كان لدينا قائمة تحتوي على ١٠,٠٠٠ عنصر، واستخدام Virtualization قلل زمن الرندر من ٤ ثوانٍ إلى ١٥٠ مللي ثانية فقط.
واحدة من أكثر الأخطاء شيوعاً في تطبيقات React هي إعادة إنشاء الدوال في كل رندر. مثلاً، إذا كان لديك مكون يحتوي على دالة تُمرر كم Props لمكون فرعي، فإن إعادة إنشاء هذه الدالة في كل رندر سيؤدي إلى إعادة رندر المكون الفرعي حتى لو استخدمت React.memo. السبب هو أن React.memo تقارن Props باستخدام المقارنة السطحية (Shallow Comparison)، والدوال الجديدة تعتبر مختلفة عن القديمة حتى لو كانت تؤدي نفس الوظيفة.
الحل هنا هو استخدام useCallback لتخزين الدوال ومنع إعادة إنشائها إلا عند تغير التبعيات. لكن مرة أخرى، احذر من الإفراط في استخدام useCallback. إذا كانت الدالة بسيطة جداً ولا تُمرر كم Props لمكونات فرعية، فقد لا يكون هناك حاجة لاستخدام useCallback. أيضاً، إذا كانت الدالة تعتمد على قيم تتغير في كل رندر (مثل الحالة المحلية)، فإن استخدام useCallback قد لا يكون فعالاً.
function ParentComponent() {
const [count, setCount] = useState(0);
// بدون useCallback، ستُعاد إنشاء هذه الدالة في كل رندر
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // لا توجد تبعيات، لذا لن تُعاد إنشاء الدالة أبداً
return (
<div>
<ChildComponent {increment} />
<p>Count: {count}</p>
</div>
);
}
const ChildComponent = React.memo(({ onIncrement }) => {
console.log('ChildComponent rendered'); // لن يُطبع إلا إذا تغيرت Props
return <button onClick={onIncrement}>زيادة</button>;
});في المثال أعلاه، استخدمنا useCallback لمنع إعادة إنشاء دالة increment في كل رندر. هذا يعني أن المكون ChildComponent لن يُعاد رندر إلا إذا تغيرت Props الأخرى (التي لا توجد في هذا المثال). في مشروع حقيقي، استخدمنا هذه التقنية في مكونات تحتوي على عشرات الدوال التي تُمرر كم Props، وكانت النتيجة تحسيناً بنسبة ٣٠٪ في زمن الرندر.
Redux هو أداة رائعة لإدارة الحالة العامة في التطبيقات الكبيرة، لكنه ليس الحل الأمثل لكل حالة. في الواقع، استخدام Redux لإدارة الحالة المحلية يمكن أن يؤدي إلى تعقيد غير ضروري ويؤثر سلباً على الأداء. المشكلة الرئيسية في Redux هي أنه مصمم ليكون مركزاً واحداً للحقيقة (Single Source of Truth)، وهذا يعني أن أي تغيير في الحالة يؤدي إلى إعادة رندر جميع المكونات المتصلة بهذه الحالة — حتى تلك التي لا تحتاج إلى هذا التغيير.
الحل هو استخدام مزيج من الحلول. استخدم Redux للحالة التي تحتاجها مكونات متعددة في أماكن مختلفة من التطبيق، واستخدم useState للحالة المحلية التي لا تحتاجها إلا مكون واحد. إذا كنت بحاجة إلى حالة مشتركة بين مكونات متجاورة، فكر في استخدام Context بدلاً من Redux. أيضاً، إذا كنت تستخدم Redux، تأكد من استخدام Selectors لتحسين الأداء، حيث تسمح لك بتحديد الأجزاء الدقيقة من الحالة التي يحتاجها كل مكون، مما يقلل من عدد عمليات إعادة الرندر غير الضرورية.
// استخدام Selectors لتحسين أداء Redux
import { createSelector } from 'reselect';
// Selector بسيط
const selectUsers = state => state.users;
// Selector معقد يستخدم memoization
const selectActiveUsers = createSelector(
[selectUsers],
users => users.filter(user => user.isActive)
);
// في المكون
function UserList() {
const activeUsers = useSelector(selectActiveUsers);
// ...
}في المثال أعلاه، استخدمنا createSelector من مكتبة reselect لإنشاء Selector معقد يقوم بتصفية المستخدمين النشطين. الميزة هنا هي أن نتيجة الفلترة سيتم تخزينها مؤقتاً (Memoized)، ولن تُعاد حسابها إلا إذا تغيرت مصفوفة المستخدمين. هذا يعني أن المكون UserList لن يُعاد رندر إلا إذا تغيرت نتيجة selectActiveUsers بالفعل. في تطبيق تعرضنا له، استخدمنا هذه التقنية لتقليل عدد عمليات إعادة الرندر من ٢٠٠ إلى ٣٠ فقط، مما أدى إلى تحسين كبير في الأداء.
Zustand هي مكتبة إدارة حالة حديثة توفر واجهة برمجة تطبيقات أبسط وأكثر كفاءة من Redux. إذا كان تطبيقك لا يحتاج إلى ميزات متقدمة مثل Middleware أو Time-Travel Debugging، فقد يكون Zustand خياراً أفضل. الميزة الرئيسية في Zustand هي أنها لا تعتمد على Context، مما يعني أنها لا تسبب إعادة رندر لجميع المكونات عند تغيير الحالة. بدلاً من ذلك، تستخدم Zustand نظام اشتراك (Subscription) يسمح للمكونات بالتفاعل فقط مع الأجزاء التي تحتاجها من الحالة.
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));
function Counter() {
const { count, increment } = useStore();
return (
<div>
<button {increment}>زيادة</button>
<p>{count}</p>
</div>
);
}في المثال أعلاه، المكون Counter سيتفاعل فقط مع تغييرات count، ولن يُعاد رندر عند تغيير أجزاء أخرى من الحالة. هذا يجعل Zustand أكثر كفاءة من Redux في معظم الحالات، خاصة في التطبيقات الصغيرة والمتوسطة. في مشروع حقيقي، انتقلنا من Redux إلى Zustand لتقليل حجم الكود بنسبة ٤٠٪ وتحسين الأداء بنسبة ٢٥٪.
واحدة من أكبر الأخطاء التي تؤدي إلى تجميد واجهة المستخدم هي تنفيذ عمليات مكثفة في سلسلة الـ Event Loop الرئيسية. مثلاً، إذا كان لديك دالة تقوم بمعالجة آلاف السجلات في مصفوفة، وتنفيذ هذه الدالة في استجابة لحدث click، فإن واجهة المستخدم ستتجمد حتى تنتهي الدالة من التنفيذ. السبب هو أن جافا سكريبت تعمل بخيط واحد (Single-Threaded)، وأي عملية تستغرق وقتاً طويلاً ستعطل سلسلة الأحداث (Event Loop) وتمنع تحديث واجهة المستخدم.
الحل هنا هو استخدام تقنيات مثل Web Workers أو تقسيم المهام إلى أجزاء صغيرة باستخدام setTimeout أو requestIdleCallback. Web Workers تسمح لك بتنفيذ الكود في خيط منفصل (Thread)، مما يعني أن سلسلة الأحداث الرئيسية لن تتعطل. أما setTimeout و requestIdleCallback فتسمح لك بتقسيم المهام الطويلة إلى أجزاء صغيرة تُنفذ في أوقات مختلفة، مما يعطي المتصفح فرصة لتحديث واجهة المستخدم بين كل جزء.
// تقسيم مهمة طويلة باستخدام setTimeout
function processLargeArray(array, callback) {
const chunkSize = 100;
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, array.length);
for (; index < end; index++) {
// معالجة العنصر
console.log(array[index]);
}
if (index < array.length) {
setTimeout(processChunk, 0); // السماح بتحديث واجهة المستخدم
} else {
callback();
}
}
processChunk();
}
// استخدام Web Worker
// worker.js
self. function(e) {
const result = heavyComputation(e.data);
postMessage(result);
};
// في المكون الرئيسي
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = function(e) {
console.log('Result:', e.data);
};في المثال الأول، استخدمنا setTimeout لتقسيم معالجة مصفوفة كبيرة إلى أجزاء صغيرة، مما يسمح للمتصفح بتحديث واجهة المستخدم بين كل جزء. في المثال الثاني، استخدمنا Web Worker لتنفيذ عملية حسابية ثقيلة في خيط منفصل. في مشروع تعرضنا له، استخدمنا Web Workers لمعالجة بيانات كبيرة في الخلفية، وكانت النتيجة تحسيناً بنسبة ٨٠٪ في استجابة واجهة المستخدم أثناء تنفيذ العمليات الثقيلة.
أخيراً، لا يمكنك تحسين ما لا يمكنك قياسه. React توفر أدوات مدمجة لمراقبة الأداء، مثل React DevTools التي تسمح لك بتسجيل وتحليل زمن الرندر لكل مكون. أيضاً، يمكنك استخدام واجهة برمجة تطبيقات Performance في المتصفح لقياس زمن التحميل والتفاعل. المفتاح هنا هو تحديد النقاط الحرجة في تطبيقك — مثل المكونات التي تُعاد رندر بشكل متكرر أو العمليات التي تستغرق وقتاً طويلاً — ثم التركيز على تحسينها.
بالإضافة إلى الأدوات المدمجة، هناك مكتبات خارجية مثل why-did-you-render التي تساعدك على تحديد سبب إعادة رندر المكونات. هذه المكتبة تضيف رسائل تحذير في وحدة التحكم عندما تُعاد رندر المكونات دون داعٍ، مما يساعدك على تحديد المشاكل بسرعة. أيضاً، لا تنسَ استخدام أدوات تحليل الأداء في المتصفح مثل Lighthouse و WebPageTest للحصول على نظرة شاملة عن أداء تطبيقك.
// استخدام React DevTools Profiler
import { Profiler } from 'react';
function App() {
const (id, phase, actualDuration) => {
console.log(`${id} took ${actualDuration}ms to render`);
};
return (
<Profiler id="App" onRender={onRender}>
{/* مكونات التطبيق */}
</Profiler>
);
}
// استخدام why-did-you-render
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
trackAllPureComponents: true,
});في المثال الأول، استخدمنا Profiler من React لتسجيل زمن الرندر لكل مكون. في المثال الثاني، استخدمنا مكتبة why-did-you-render لتحديد سبب إعادة رندر المكونات. في مشروع حقيقي، استخدمنا هذه الأدوات لتحديد مكونات كانت تُعاد رندر أكثر من ٥٠ مرة في الثانية، وقمنا بتحسينها باستخدام التقنيات المذكورة في هذا المقال، وكانت النتيجة تحسيناً بنسبة ٥٠٪ في زمن الرندر الكلي.
تحسين أداء React ليس مهمة تقوم بها مرة واحدة ثم تنساها. إنها عملية مستمرة تبدأ من اليوم الأول لكتابة الكود. القاعدة الذهبية هي: لا تنتظر حتى يصبح التطبيق بطيئاً لتبدأ في التفكير في الأداء. بدلاً من ذلك، اجعل تحسين الأداء جزءاً من عملية التطوير اليومية. استخدم الأدوات المتاحة لمراقبة الأداء بانتظام، وقم بمراجعة الكود بشكل دوري لتحديد النقاط الحرجة. تذكر أن كل مللي ثانية تحسب، وكل رندر غير ضروري هو هدر للموارد. في النهاية، المستخدم لا يهتم إذا كنت تستخدم أحدث مكتبات أو تقنيات البرمجة — كل ما يهمه هو أن التطبيق يعمل بسرعة وسلاسة. اجعل هذا هدفك، وستجد أن تحسين الأداء يصبح جزءاً طبيعياً من عملية التطوير، وليس مهمة شاقة تقوم بها في اللحظة الأخيرة.
نصيحة أخيرة: ابدأ بقياس الأداء اليوم. استخدم React DevTools لتحديد المكونات التي تُعاد رندر بشكل متكرر، واستخدم أدوات تحليل الأداء في المتصفح لتحديد نقاط الاختناق. ثم طبق التقنيات المذكورة في هذا المقال واحدة تلو الأخرى، وقِس التأثير بعد كل تغيير. ستندهش من مقدار التحسن الذي يمكنك تحقيقه ببضعة تعديلات بسيطة. الأداء الجيد ليس صدفة — إنه نتيجة للتفكير الدقيق والكود المكتوب بعناية.