زیادہ تر React پرفارمنس ٹیوٹوریلز ایک ہی غلط مشورے پر ختم ہوتے ہیں: ہر چیز کو useMemo اور useCallback میں لپیٹ دیں اور کام مکمل سمجھ لیں۔ اگر آپ نے اس مشورے پر عمل کیا ہے، تو غالباً آپ نے اپنی ایپلی کیشن کو مزید سست کر دیا ہے۔ یہ hooks مفت نہیں ہیں۔ ہر ایک میموری مختص کرتا ہے، dependencies کا موازنہ کرتا ہے، اور cached values کو محفوظ کرتا ہے۔ مقصد کے بغیر استعمال کرنے پر، یہ optimization کے بجائے overhead بن جاتے ہیں۔
آئیے اس کو اس حد تک محدود کرتے ہیں جو اصل میں اہمیت رکھتا ہے۔
ہر Hook اصل میں کیا کرتا ہے
useMemo ایک value کو یاد رکھتا ہے۔ آپ اسے ایک ایسا function دیتے ہیں جو بھاری کام کرتا ہے، اور یہ نتیجہ واپس کرتا ہے۔ اگلے render پر، اگر آپ کی dependencies تبدیل نہیں ہوئی ہیں، تو React حساب کتاب (calculation) کو چھوڑ دیتا ہے اور پرانا نتیجہ واپس کر دیتا ہے۔
useCallback ایک function کو یاد رکھتا ہے۔ یہ آپ کے لیے function کو چلا کر نہیں دیتا۔ یہ صرف renders کے درمیان وہی function instance واپس کرتا ہے جب تک کہ اس کی dependencies ایک جیسی رہیں۔
یہی اصل فرق ہے۔ ایک computed value کو cache کرتا ہے۔ دوسرا reference کو cache کرتا ہے۔ ان دونوں کو مکس کرنے سے ایسا کوڈ بنتا ہے جو دیکھنے میں تو optimized لگتا ہے لیکن اس کا رویہ unwrapped کوڈ جیسا ہی ہوتا ہے، جبکہ یہ اضافی میموری کا خرچ کرتا ہے۔
Function Identity آپ کے Tree کو کیوں خراب کرتی ہے
جب ایک component re-render ہوتا ہے، تو React پورے function body کو دوبارہ چلا دیتا ہے۔ ہر variable دوبارہ تخلیق ہوتا ہے۔ ہر inline function کو میموری میں ایک بالکل نیا ایڈریس ملتا ہے۔
JavaScript میں، دو functions جن میں بالکل ایک جیسی logic ہو، وہ برابر نہیں ہوتے۔ () => {} === () => {} کا نتیجہ false ہوتا ہے۔ یہی اصول objects اور arrays پر بھی لاگو ہوتا ہے۔ اگر آپ کا parent component handleSubmit کو define کرتا ہے اور اسے child کو پاس کرتا ہے، تو وہ child ہر render پر ایک نیا prop وصول کرتا ہے۔ اگر child React.memo میں لپٹا ہوا (wrapped) بھی ہو، تب بھی وہ یہ نہیں پہچان سکتا کہ نیا function وہی کام کر رہا ہے جو پرانا کر رہا تھا۔ reference بدل گیا، اس لیے child re-render ہو گیا۔
یہی وہ بنیادی مسئلہ ہے جسے حل کرنے کے لیے useCallback بنایا گیا تھا۔ یہ رفتار (speed) کے بارے میں نہیں ہے۔ یہ استحکام (stability) کے بارے میں ہے۔
useMemo کب کارآمد ثابت ہوتا ہے
آپ کو useMemo کی ضرورت تب ہوتی ہے جب آپ ایسا کام کر رہے ہوں جو واضح طور پر مہنگا (expensive) ہو اور آپ اس کے نتیجے میں محسوس کیے جانے والے وقفے (lag) کو دیکھ سکیں۔
ایک بہت بڑے dataset کو filter کرنے کے بارے میں سوچیں۔ اگر آپ کے پاس دسیوں ہزار rows والی ایک table اور ایک search input ہے، تو آپ اپنے component کے اندر کچھ اس طرح لکھ سکتے ہیں:
const visibleRows = rows.filter(r => r.name.includes(query));
useMemo کے بغیر، وہ loop ہر render پر چلتا ہے۔ اگر صارف کسی ایسے بٹن پر کلک کرتا ہے جو sidebar کو toggle کرتا ہے، تو parent re-render ہوتا ہے، اور آپ کا filter دوبارہ چلتا ہے حالانکہ rows اور query کبھی تبدیل نہیں ہوئے۔ ایک بڑے dataset پر، وہ جھٹکا (stutter) واضح طور پر نظر آتا ہے۔
useMemo نتیجے کو مستقل (pinning) کر کے اسے ٹھیک کر دیتا ہے:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
اب React اس filter کو صرف تب ہی دوبارہ چلاتا ہے جب dependencies اصل میں تبدیل ہوں۔
یہی منطق پیچیدہ ریاضیاتی حساب کتاب (mathematical calculations)، API responses کو chart-friendly formats میں تبدیل کرنے، یا ایسی state اخذ کرنے پر بھی لاگو ہوتی ہے جسے ورنہ مسلسل دوبارہ کیلکولیٹ کیا جاتا۔
ایک دوسرا، کم واضح استعمال بھی ہے۔ اگر آپ مقامی طور پر (locally) کوئی object یا array بناتے ہیں اور اسے useEffect dependency array میں شامل کرتے ہیں، تو آپ غلطی سے ہر render پر اس effect کو trigger کر سکتے ہیں۔ Inline objects اور arrays کو ہر بار نئی identities ملتی ہیں، اس لیے effect ایک بدلی ہوئی dependency دیکھتا ہے اور دوبارہ چل جاتا ہے۔ اس object کو useMemo کے ساتھ memoize کرنے سے reference مستحکم رہتا ہے اور آپ کا effect صرف تب ہی چلتا ہے جب اصل ڈیٹا تبدیل ہو۔
useCallback کب ضروری ہو جاتا ہے
useCallback سب سے زیادہ اس وقت اہمیت رکھتا ہے جب آپ handlers کو ان child components میں پاس کر رہے ہوں جو React.memo کے ساتھ optimized ہوں۔
ایک parent component کا تصور کریں جو ایک counter رکھتا ہے۔ یہ ایک مہنگا (expensive) child list بھی render کرتا ہے:
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={() => setCount(c + 1)}>{count}</button>
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
ہر بار جب count تبدیل ہوتا ہے، Parent re-render ہوتا ہے۔ ایک نیا handleItemClick تخلیق ہوتا ہے۔ چونکہ ExpensiveList کو ایک نیا prop reference ملتا ہے، اس لیے وہ بھی re-render ہو جاتا ہے۔ اگر ExpensiveList کو React.memo میں لپٹا ہوا ہو، تو وہ memoization مکمل طور پر ضائع ہو جاتی ہے کیونکہ function prop تبدیل ہو گیا تھا۔
useCallback reference کو محفوظ رکھتا ہے:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
اب ExpensiveList صرف تب ہی re-render ہوتا ہے جب اسے واقعی ضرورت ہو۔
ایک اور اہم صورتحال useEffect سے متعلق ہے۔ اگر کوئی effect آپ کے component کے اندر تعریف شدہ (defined) کسی function کو subscribe کرتا ہے، اور وہ function ہر render پر اپنی identity تبدیل کرتا ہے، تو effect بار بار teardown اور resubscribe ہوگا۔ Function کو memoize کرنا effect کو مستحکم رکھتا ہے۔
Dependency Array کا جال اور Stale Closures
دونوں hooks dependency arrays پر انحصار کرتے ہیں، اور یہیں زیادہ تر bugs چھپے ہوتے ہیں۔
اگر آپ dependency array سے کسی variable کو نکال دیتے ہیں، تو آپ کا memoized function یا value اس variable کے پرانے ورژن پر کلوز ہو جاتی ہے۔ اسے stale closure کہا جاتا ہے۔ UI شاید تازہ ڈیٹا دکھا رہا ہو، لیکن آپ کا callback اب بھی تین renders پہلے والے state کو دیکھ رہا ہوتا ہے۔ اس کا حل سادہ ہے لیکن code reviews کے دوران اسے نظر انداز کرنا آسان ہے: hook کے اندر استعمال ہونے والی ہر اس value کو شامل کریں جو تبدیل ہو سکتی ہے۔
react-hooks/exhaustive-deps ESLint rule کو چلائیں۔ یہ واضح کوتاہیوں کو پکڑ لے گا۔ لیکن اسے ایک روبوٹ کی طرح نہ سمجھیں۔ یہ سمجھیں کہ ہر dependency کیوں اہم ہے۔
ضرورت سے زیادہ optimization کی پوشیدہ قیمت
مبتدی (Beginners) اکثر ہر function اور ہر value کو ان hooks کے ساتھ محفوظ کرنے کی کوشش کرتے ہیں کیونکہ یہ محفوظ محسوس ہوتا ہے۔ یہ عادت الٹی پڑ جاتی ہے۔
React کو memory میں cached values کو محفوظ کرنا پڑتا ہے۔ ہر render پر، اسے آپ کے dependency array میں موجود ہر item کو Object.is کے ذریعے موازنہ (compare) کرنا پڑتا ہے۔ یہ موازنہ سستا تو ہے، لیکن یہ مفت نہیں ہے۔ اگر آپ onClick={() => setOpen(true)} جیسے معمولی event handler کو useCallback کے اندر لپیٹتے (wrap کرتے) ہیں، تو آپ ایک ایسے function کو بنانے سے بچنے کے لیے memory اور CPU کی قیمت ادا کر رہے ہیں جسے بنانا فوری طور پر ممکن تھا۔
یہ hooks کوڈ میں غیر ضروری پیچیدگی (noise) بھی پیدا کرتے ہیں۔ useMemo اور useCallback میں لپٹا ہوا کوڈ پڑھنے اور اسے برقرار رکھنا (maintain) مشکل ہوتا ہے۔ ہر dependency array ایک ممکنہ stale closure ہے جو آپ کو نقصان پہنچانے کا انتظار کر رہا ہے۔
اصل اصول (rule of thumb) پرکشش نہیں ہے لیکن مؤثر ہے: پہلے سادہ کوڈ لکھیں۔ صرف تب optimize کریں جب آپ کے پاس کسی مسئلے کا ثبوت ہو۔ یہ پہچاننے کے لیے کہ کون سے components مہنگے ہیں اور کون سے renders فضول ہیں، React DevTools Profiler کا استعمال کریں۔ اگر ایک render چند ملی سیکنڈ سے بھی کم وقت لیتا ہے، تو کوئی صارف اس پر غور نہیں کرے گا، اور آپ کی memoization کسی مسئلے کا حل نہیں نکال رہی ہوگی۔
حتمی بات
useMemo مہنگی values کے لیے ہے۔ useCallback مستحکم (stable) function references کے لیے ہے۔ کوئی بھی hook خود بخود آپ کے component کے render ہونے کی رفتار کو تیز نہیں کرتا؛ وہ صرف غیر ضروری downstream کام کو روکتے ہیں۔ ان کے بغیر شروع کریں، اصل tooling کے ذریعے پیمائش کریں، اور انہیں بالکل وہیں شامل کریں جہاں profiler کسی رکاوٹ (bottleneck) کی نشاندہی کرے۔ کبھی کبھار rerender ہونے والا صاف ستھرا کوڈ، اس over-engineered کوڈ سے ہمیشہ بہتر ہوگا جو ہر چیز کو memoize کرتا ہے۔
