अधिकांश React परफॉरमेंस ट्यूटोरियल एक ही गलत सलाह के साथ समाप्त होते हैं: सब कुछ useMemo और useCallback में रैप (wrap) कर दें और काम खत्म समझें। यदि आपने उस सलाह का पालन किया है, तो आपने संभवतः अपने एप्लिकेशन को धीमा कर दिया है। ये हुक्स मुफ्त नहीं हैं। प्रत्येक मेमोरी आवंटित करता है, डिपेंडेंसीज़ (dependencies) की तुलना करता है, और कैश किए गए मानों (cached values) को स्टोर करता है। बिना किसी उद्देश्य के उपयोग किए जाने पर, वे ऑप्टिमाइज़ेशन के बजाय ओवरहेड बन जाते हैं।

आइए इसे केवल उस बात तक सीमित करें जो वास्तव में मायने रखती है।

प्रत्येक हुक वास्तव में क्या करता है

useMemo एक वैल्यू (value) को याद रखता है। आप इसे एक ऐसा फंक्शन देते हैं जो भारी काम करता है, और यह परिणाम (result) लौटाता है। अगले रेंडर पर, यदि आपकी डिपेंडेंसीज़ नहीं बदली हैं, तो React गणना (calculation) को छोड़ देता है और पुराना परिणाम वापस दे देता है।

useCallback एक फंक्शन (function) को याद रखता है। यह आपके लिए फंक्शन को चलाता नहीं है। यह रेंडर्स के बीच केवल उसी फंक्शन इंस्टेंस (instance) को लौटाता है, जब तक कि इसकी डिपेंडेंसीज़ समान रहती हैं।

यही पूरा अंतर है। एक कंप्यूटेड वैल्यू को कैश करता है। दूसरा एक रेफरेंस (reference) को कैश करता है। इनमें भ्रमित होने से ऐसा कोड बनता है जो ऑप्टिमाइज़्ड तो दिखता है, लेकिन बिना रैप किए गए कोड की तरह ही व्यवहार करता है और साथ ही अतिरिक्त मेमोरी की लागत भी लगाता है।

फंक्शन आइडेंटिटी (Function Identity) आपके ट्री को क्यों तोड़ती है

जब कोई कंपोनेंट री-रेंडर होता है, तो React पूरे फंक्शन बॉडी को फिर से निष्पादित (execute) करता है। प्रत्येक वेरिएबल को फिर से बनाया जाता है। प्रत्येक इनलाइन फंक्शन को मेमोरी में एक बिल्कुल नया एड्रेस मिलता है।

JavaScript में, बिल्कुल समान लॉजिक वाले दो फंक्शन बराबर नहीं होते हैं। () => {} === () => {} का परिणाम false होता है। यही नियम ऑब्जेक्ट्स और एरेज़ (arrays) पर भी लागू होता है। यदि आपका पैरेंट कंपोनेंट handleSubmit को परिभाषित करता है और उसे चाइल्ड को पास करता है, तो वह चाइल्ड हर रेंडर पर एक नया प्रॉप (prop) प्राप्त करता है। भले ही चाइल्ड React.memo में रैप किया गया हो, वह यह नहीं बता सकता कि नया फंक्शन पुराने वाले की तरह ही काम करता है। रेफरेंस बदल गया, इसलिए चाइल्ड री-रेंडर होता है।

यही वह मूल समस्या है जिसे हल करने के लिए useCallback बनाया गया था। यह स्पीड के बारे में नहीं है। यह स्थिरता (stability) के बारे में है।

useMemo कब वास्तव में उपयोगी साबित होता है

आपको useMemo की आवश्यकता तब होती है जब आप ऐसा काम कर रहे हों जो वस्तुतः महंगा (expensive) हो और आप एक मापने योग्य लैग (lag) देख सकें।

एक विशाल डेटासेट को फ़िल्टर करने के बारे में सोचें। यदि आपके पास हज़ारों पंक्तियों (rows) वाली एक टेबल और एक सर्च इनपुट है, तो आप अपने कंपोनेंट के अंदर कुछ ऐसा लिख सकते हैं:

const visibleRows = rows.filter(r => r.name.includes(query));

useMemo के बिना, वह लूप हर रेंडर पर चलता है। यदि उपयोगकर्ता किसी ऐसे बटन पर क्लिक करता है जो साइडबार को टॉगल करता है, तो पैरेंट री-रेंडर होता है, और आपका फ़िल्टर फिर से चलता है भले ही rows और query कभी न बदले हों। एक बड़े डेटासेट पर, वह रुकावट (stutter) साफ़ दिखाई देती है।

useMemo परिणाम को पिन (pin) करके इसे ठीक करता है:

const visibleRows = useMemo(() => {
  return rows.filter(r => r.name.includes(query));
}, [rows, query]);

अब React उस फ़िल्टर को तभी फिर से चलाता है जब डिपेंडेंसीज़ वास्तव में बदलती हैं।

यही तर्क जटिल गणितीय गणनाओं, API रिस्पॉन्स को चार्ट-फ्रेंडली फॉर्मेट में बदलने, या ऐसे स्टेट (state) को प्राप्त करने पर भी लागू होता है जिसे अन्यथा लगातार पुनर्गणना (recalculate) किया जाएगा।

एक दूसरा, कम स्पष्ट उपयोग का मामला भी है। यदि आप स्थानीय रूप से (locally) एक ऑब्जेक्ट या एरे बनाते हैं और उसे useEffect डिपेंडेंसी एरे में शामिल करते हैं, तो आप गलती से हर रेंडर पर उस इफेक्ट को ट्रिगर कर सकते हैं। इनलाइन ऑब्जेक्ट्स और एरेज़ को हर बार नई पहचान (identities) मिलती है, इसलिए इफेक्ट बदली हुई डिपेंडेंसी देखता है और फिर से चलता है। useMemo के साथ उस ऑब्जेक्ट को मेमोइज़ (memoize) करने से रेफरेंस स्थिर रहता है और आपका इफेक्ट केवल तभी चलता है जब अंतर्निहित डेटा वास्तव में बदलता है।

useCallback कब आवश्यक हो जाता है

useCallback सबसे अधिक तब मायने रखता है जब आप उन चाइल्ड कंपोनेंट्स में हैंडलर्स (handlers) पास कर रहे हों जो React.memo के साथ ऑप्टिमाइज़ किए गए हैं।

एक पैरेंट कंपोनेंट की कल्पना करें जिसमें एक काउंटर है। यह एक महंगा चाइल्ड लिस्ट भी रेंडर करता है:

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 री-रेंडर होता है। एक नया handleItemClick बनाया जाता है। क्योंकि ExpensiveList को एक नया प्रॉप रेफरेंस मिलता है, इसलिए यह भी री-रेंडर होता है। यदि ExpensiveList को React.memo में रैप किया गया है, तो वह मेमोइज़ेशन पूरी तरह से बेकार हो जाता है क्योंकि फंक्शन प्रॉप बदल गया है।

useCallback रेफरेंस को सुरक्षित रखता है:

const handleItemClick = useCallback((id) => {
  console.log(id);
}, []);

अब ExpensiveList केवल तभी री-रेंडर होता है जब उसे वास्तव में आवश्यकता होती है।

एक अन्य महत्वपूर्ण स्थिति useEffect से जुड़ी है। यदि कोई इफेक्ट आपके कंपोनेंट के अंदर परिभाषित किसी फंक्शन को सब्सक्राइब करता है, और वह फंक्शन हर रेंडर पर अपनी पहचान बदल देता है, तो इफेक्ट बार-बार टीयरडाउन (teardown) और री-सब्सक्राइब होगा। फंक्शन को मेमोइज़ करने से इफेक्ट स्थिर रहता है।

डिपेंडेंसी एरे ट्रैप और स्टेल क्लोजर्स (Stale Closures)

दोनों हुक्स डिपेंडेंसी एरेज़ पर निर्भर करते हैं, और यहीं अधिकांश बग्स छिपे होते हैं।

यदि आप dependency array से किसी variable को छोड़ देते हैं, तो आपका memoized function या value उस variable के पुराने version पर close हो जाती है। इसे stale closure कहते हैं। UI ताज़ा डेटा दिखा सकता है, लेकिन आपका callback अभी भी तीन renders पहले वाले state को देख रहा होता है। इसका समाधान सरल है लेकिन code reviews के दौरान इसे नज़रअंदाज़ करना आसान है: hook के अंदर उपयोग किए जाने वाले हर उस value को शामिल करें जो बदल सकती है।

react-hooks/exhaustive-deps ESLint rule चलाएँ। यह स्पष्ट omissions को पकड़ लेगा। लेकिन इसे एक रोबोट की तरह न समझें। समझें कि प्रत्येक dependency क्यों महत्वपूर्ण है।

Over-Optimization की छिपी हुई कीमत

शुरुआती लोग अक्सर हर function और हर value को इन hooks से सुरक्षित करने की कोशिश करते हैं क्योंकि यह सुरक्षित महसूस होता है। यह आदत उल्टा असर करती है।

React को memory में cached values को स्टोर करना पड़ता है। हर render पर, इसे आपके dependency array में iterate करना होता है और Object.is का उपयोग करके प्रत्येक item की तुलना करनी होती है। वह तुलना सस्ती है, लेकिन मुफ्त नहीं है। यदि आप onClick={() => setOpen(true)} जैसे एक मामूली event handler को useCallback के अंदर wrap करते हैं, तो आप एक ऐसे function को बनाने से बचने के लिए memory और CPU की कीमत चुका रहे हैं जिसे allocate करना तुरंत हो जाता।

Hooks शोर (noise) भी बढ़ाते हैं। useMemo और useCallback में wrap किया गया code पढ़ने और maintain करने में कठिन होता है। प्रत्येक dependency array एक संभावित stale closure है जो आपको नुकसान पहुँचाने का इंतज़ार कर रहा है।

असली व्यावहारिक नियम (rule of thumb) आकर्षक नहीं है लेकिन प्रभावी है: पहले साधारण code लिखें। केवल तभी optimize करें जब आपके पास किसी समस्या का प्रमाण हो। यह पहचानने के लिए कि कौन से components महंगे हैं और कौन से renders व्यर्थ हैं, React DevTools Profiler का उपयोग करें। यदि एक render में कुछ मिलीसेकंड से भी कम समय लगता है, तो कोई भी user इसे नोटिस नहीं करेगा, और आपकी memoization कुछ भी हल नहीं कर रही है।

निष्कर्ष

useMemo महंगे values के लिए है। useCallback stable function references के लिए है। कोई भी hook अपने आप आपके component को तेज़ी से render नहीं करता; वे अनावश्यक downstream work को रोकते हैं। इनके बिना शुरुआत करें, वास्तविक tooling के साथ मापें, और उन्हें ठीक वहीं जोड़ें जहाँ profiler bottleneck दिखाता है। कभी-कभी rerender होने वाला clean code, उस over-engineered code से लगभग हमेशा बेहतर होगा जो हर चीज़ को memoize करता है।