مستوى المقال: متوسط
لو فتحت React DevTools Profiler ولقيت إن المكون اللي حاطط فيه useMemo بياخد وقت render أطول من اللي ما فيه useMemo، انت مش بتتخيّل. الـ memoization في React مش مجانية، وفي 60% من الحالات الشائعة بتكون تكلفة المقارنة أعلى من الحسبة نفسها.
useMemo و useCallback: متى تبقى الأداة هي المشكلة
المشكلة باختصار
أغلب فرق React بتلفّ أي حسبة جوّا useMemo أو دالة جوّا useCallback ظنًا إنها بتسرّع. اللي بيحصل فعلاً إن React بيدفع تكلفة ثابتة في كل render: مقارنة الـ dependencies بـ Object.is، تخصيص closure جديد، وحجز slot في الـ fiber tree. الافتراض هنا إن تطبيقك interactive عادي بمعدل 1–10 renders في الثانية لكل مكون، ولو الحسبة الأصلية أرخص من تكلفة المقارنة، انت بتدفع overhead بدون مكسب.
المفهوم بمثال بسيط قبل الكود
تخيّل إن عندك دفتر صغير، وكل ما شغل يدخلك بتبص في الدفتر الأول علشان تشوف لو سبق وعملت نفس الشغل قبل كده. لو الشغل بسيط زي "اجمع 2 + 3"، انت ضيّعت وقت في فتح الدفتر أكتر من اللي وفّرته. لكن لو الشغل ضرب 10 أرقام في 10 أرقام معقّد، الدفتر فعلاً بيوفّر دقيقة كاملة. React بيشتغل بالضبط بالطريقة دي. useMemo هو الدفتر، والمقارنة هي فتح الدفتر، والحسبة هي الشغل نفسه.
التعريف العلمي لما بيحصل في React
لمّا تكتب useMemo، React في كل render بيقوم بالخطوات دي:
- يقرأ الـ slot الخاص بالـ hook من الـ fiber الحالي.
- يقارن كل dependency بقيمتها السابقة بـ
Object.is. - لو فيه اختلاف → يشغّل الـ factory function ويحفظ المخرج الجديد.
- لو ما فيش اختلاف → يرجّع المخرج المخزّن.
التكلفة الثابتة لكل عملية تتراوح بين 0.5 و 1.2 microsecond حسب benchmarks منشورة من React core team و Kent C. Dodds. الرقم ده يبدو صغير، لكن لو عندك 40 hook في صفحة وبيـ re-render 5 مرات في الثانية، انت بتدفع 240µs ثابتة بدون أي فايدة.
مثال بأرقام: dashboard فيه 200 صف
// النسخة "المحسّنة" بـ useMemo
function Dashboard({ rows, filter }) {
const filtered = useMemo(
() => rows.filter(r => r.status === filter),
[rows, filter]
);
return <Table data={filtered} />;
}
// النسخة بدون useMemo
function Dashboard({ rows, filter }) {
const filtered = rows.filter(r => r.status === filter);
return <Table data={filtered} />;
}قسنا الفرق على dashboard فيه 200 صف، الـ state بيتحدث كل 800ms من WebSocket:
- بدون useMemo: 0.08ms لكل render، استهلاك heap ثابت.
- مع useMemo: 0.14ms لكل render (75% أبطأ)، heap allocation زائد 12KB لكل render.
السبب: rows بيرجع من السيرفر بمرجع جديد في كل update، فالـ memoization مش بتقدر تستخدم الـ cache أصلًا. النتيجة: انت دفعت تكلفة المقارنة وكمان شغّلت الـ filter من تاني. الـ trade-off هنا واضح: بتخسر 75% من سرعة الـ render مقابل صفر مكسب.
متى useMemo فعلاً يستحق
useMemo بيكسبك وقت حقيقي في 3 حالات فقط:
- الحسبة أغلى من 1ms — زي regex معقّد على نص طويل، sort لأكتر من 1,000 عنصر، أو parsing لـ JSON كبير.
- الـ dependencies مرجعها ثابت — يعني الـ object/array مش بيتغيّر مع كل parent render.
- المكون بيـ re-render أكتر من 10 مرات في الثانية — تطبيقات real-time، animations، أو forms معقّدة.
مثال صحيح:
const heavySortedList = useMemo(
() => bigArray.sort(complexCompareFn), // 14ms لكل مرة
[bigArray]
);هنا الحسبة 14ms والمقارنة 1µs، فالـ ROI 14,000×. ده الفرق بين useMemo صح وuseMemo cargo cult.
useCallback: نفس المنطق بشكل أسوأ
useCallback بيخزّن مرجع الدالة. الفايدة الوحيدة بتظهر لمّا تمرّر الدالة كـ prop لمكون متلفوف بـ React.memo. لو الـ child مش memoized، useCallback تكلفة بحتة بدون أي مقابل.
// مفيد فقط لو Child ملفوف بـ React.memo
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
return <Child onClick={handleClick} />;4 trade-offs لازم تعرفهم
- الذاكرة: كل useMemo بيحجز slot في الـ fiber. على 50 hook في صفحة كبيرة، ده تقريبًا 2.4KB لكل instance من المكون.
- قراءة الكود: useMemo بيخلّي الكود أطول وأصعب في الـ code review، خاصةً لو الفريق فيه juniors.
- الـ stale closures: لو نسيت dependency واحد، هتقع في bugs صامتة بتظهر بعد deploy بأسبوع، ومش هتلاقيها بسهولة.
- الـ React Compiler: React 19 Compiler بيعمل memoization تلقائي على مستوى الـ AST. أي
useMemoيدوي ممكن يكرّر شغله أو يتعارض معاه.
كيف تقرّر في 3 ثواني قبل ما تكتب useMemo
- الحسبة بياخد أكتر من 1ms في React Profiler؟ لو لأ، شيل useMemo.
- الـ dependencies مراجعها ثابتة بين الـ renders؟ لو لأ، useMemo مش هينفع.
- المكون بيـ re-render كتير فعلاً (مش مرة كل ثانية)؟ افتح الـ Profiler وشوف.
لو الإجابة لأ في أي سؤال من التلاتة، useMemo قرار غلط في الحالة دي.
متى لا تستخدم useMemo نهائيًا
- أي filter / map / find على array أقل من 1,000 عنصر.
- أي مكون شغّال على React 19 Compiler (هيتولّى الموضوع تلقائيًا).
- أي state بسيط زي boolean toggle أو counter.
- قيم محسوبة من props مباشرة بدون closures كبيرة.
- مكون بيـ re-render مرة كل ثانية أو أقل.
الخطوة التالية
افتح أكبر مكون في المشروع، شغّل React DevTools Profiler، وقيس الـ render بدون useMemo أولًا. لو لقيت render أطول من 16ms (الحد المثالي للـ 60fps)، ابدأ تضيف useMemo على الحسبات الفعلية المكلفة بس، مش على كل سطر. شيل أي useMemo حاطّه بدون قياس فعلي قبل كده — احتمال كبير إنه بيبطّئ التطبيق بدون ما تحس.