મોટાભાગના React પરફોર્મન્સ ટ્યુટોરિયલ્સ એક જ ખરાબ સલાહ સાથે સમાપ્ત થાય છે: બધું જ useMemo અને useCallback માં wrap કરી દો અને કામ પૂરું કરો. જો તમે તે સલાહ માની હોય, તો તમે કદાચ તમારા એપ્લિકેશનને વધુ ધીમું કરી દીધું હશે. આ hooks મફત નથી હોતા. દરેક hook મેમરી ફાળવે છે, dependencies ની સરખામણી કરે છે, અને cached values સ્ટોર કરે છે. હેતુ વગર ઉપયોગ કરવાથી, તેઓ optimization ને બદલે overhead બની જાય છે.

ચાલો આને સાવ સરળ બનાવીએ અને જે ખરેખર મહત્વનું છે તેના પર ધ્યાન કેન્દ્રિત કરીએ.

દરેક Hook ખરેખર શું કરે છે

useMemo એક value ને યાદ રાખે છે. તમે તેને ભારે કામ કરતી ફંક્શન આપો છો, અને તે પરિણામ (result) પરત કરે છે. આગામી render પર, જો તમારી dependencies બદલાઈ નથી, તો React ગણતરી કરવાનું ટાળે છે અને જૂનું પરિણામ પરત આપે છે.

useCallback એક function ને યાદ રાખે છે. તે તમારા માટે ફંક્શન ચલાવતું નથી. તે ફક્ત render વચ્ચે તે જ function instance પરત કરે છે, જ્યાં સુધી તેની dependencies સમાન રહે છે.

આ જ સંપૂર્ણ તફાવત છે. એક ગણતરી કરેલી value ને cache કરે છે. બીજું એક reference ને cache કરે છે. આ બંને વચ્ચે ભૂલ કરવાથી એવો કોડ બને છે જે optimized દેખાય છે પરંતુ unwrapped કોડ જેવું જ વર્તન કરે છે અને સાથે સાથે વધારાની મેમરીનો ખર્ચ પણ કરે છે.

Function Identity તમારા Tree ને કેમ તોડી નાખે છે

જ્યારે કોઈ component re-render થાય છે, ત્યારે React આખી function body ફરીથી એક્ઝિક્યુટ કરે છે. દરેક variable ફરીથી બનાવવામાં આવે છે. દરેક inline function ને મેમરીમાં એક નવું એડ્રેસ મળે છે.

JavaScript માં, સમાન લોજિક ધરાવતા બે ફંક્શન સમાન હોતા નથી. () => {} === () => {} નું મૂલ્ય false આવે છે. આ જ નિયમ objects અને arrays ને પણ લાગુ પડે છે. જો તમારું parent component handleSubmit વ્યાખ્યાયિત કરે અને તેને child ને પાસ કરે, તો તે child ને દરેક render પર નવો prop મળે છે. ભલે child ને React.memo માં wrap કરવામાં આવ્યું હોય, તે જાણી શકતું નથી કે નવું ફંક્શન જૂના ફંક્શન જેવું જ કામ કરે છે. Reference બદલાઈ ગયો હોવાથી, child ફરીથી re-render થાય છે.

આ જ મૂળ સમસ્યા છે જેને ઉકેલવા માટે useCallback બનાવવામાં આવ્યું હતું. આ ઝડપ (speed) વિશે નથી, પણ સ્થિરતા (stability) વિશે છે.

useMemo ક્યારે ખરેખર ઉપયોગી સાબિત થાય છે

તમારે useMemo ની જરૂર ત્યારે પડે છે જ્યારે તમે એવું કામ કરી રહ્યા હોવ જે ખરેખર ખર્ચાળ (expensive) હોય અને તમને માપી શકાય તેવો લેગ (lag) દેખાતો હોય.

એક વિશાળ dataset ને filter કરવા વિશે વિચારો. જો તમારી પાસે હજારો rows ધરાવતું ટેબલ અને એક search input હોય, તો તમે તમારા component ની અંદર કંઈક આવું લખી શકો છો:

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

useMemo વગર, તે loop દરેક render પર ચાલે છે. જો યુઝર એવું બટન ક્લિક કરે જે sidebar ને toggle કરે છે, તો parent re-render થાય છે, અને rows અને query ક્યારેય બદલાયા નથી હોવા છતાં તમારું filter ફરીથી ચાલે છે. મોટા dataset પર, આ અટકળ (stutter) સ્પષ્ટ દેખાય છે.

useMemo પરિણામને પિન (pin) કરીને આ સમસ્યા સુધારે છે:

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

હવે React તે filter ને ત્યારે જ ફરીથી ચલાવે છે જ્યારે dependencies ખરેખર બદલાય છે.

આ જ લોજિક જટિલ ગાણિતિક ગણતરીઓ, API responses ને chart-friendly formats માં રૂપાંતરિત કરવા, અથવા એવા state મેળવવા માટે પણ લાગુ પડે છે જે અન્યથા સતત ફરીથી ગણવામાં આવે.

બીજો એક ઓછો સ્પષ્ટ ઉપયોગ પણ છે. જો તમે સ્થાનિક રીતે (locally) કોઈ object અથવા array બનાવો છો અને તેને useEffect dependency array માં સામેલ કરો છો, તો તમે અજાણતા દરેક render પર તે effect ને trigger કરી શકો છો. Inline objects અને arrays ને દરેક વખતે નવી identity મળે છે, તેથી effect ને બદલાયેલી dependency દેખાય છે અને તે ફરીથી ફાયર થાય છે. useMemo સાથે તે object ને memoize કરવાથી reference સ્થિર રહે છે અને તમારું effect ત્યારે જ ચાલે છે જ્યારે મૂળ ડેટા ખરેખર બદલાય છે.

useCallback ક્યારે જરૂરી બને છે

useCallback ત્યારે સૌથી વધુ મહત્વનું છે જ્યારે તમે React.memo સાથે optimized કરેલા child components માં handlers પાસ કરી રહ્યા હોવ.

એક 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 માં wrap કરવામાં આવ્યું હોય, તો તે memoization સંપૂર્ણપણે વ્યર્થ જાય છે કારણ કે function prop બદલાઈ ગયો છે.

useCallback reference ને જાળવી રાખે છે:

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

હવે ExpensiveList ત્યારે જ re-render થાય છે જ્યારે તેને ખરેખર જરૂર હોય.

બીજી એક ગંભીર પરિસ્થિતિ useEffect સાથે જોડાયેલી છે. જો કોઈ effect તમારા component ની અંદર વ્યાખ્યાયિત કરેલા function ને subscribe કરે છે, અને તે function દરેક render પર તેની identity બદલે છે, તો effect વારંવાર teardown અને resubscribe થશે. Function ને memoize કરવાથી effect સ્થિર રહે છે.

Dependency Array નો ફાંસો અને Stale Closures

બંને hooks dependency arrays પર આધાર રાખે છે, અને અહીં જ મોટાભાગના bugs છુપાયેલા હોય છે.

જો તમે dependency array માંથી કોઈ વેરિએબલ કાઢી નાખો છો, તો તમારું memoized function અથવા value તે વેરિએબલના જૂના વર્ઝન પર આધારિત (close over) થઈ જાય છે. આ એક stale closure છે. UI કદાચ તાજો ડેટા બતાવે છે, પરંતુ તમારું callback હજુ પણ ત્રણ renders પહેલાના state ને જોઈ રહ્યું છે. આનો ઉકેલ સરળ છે પરંતુ code reviews દરમિયાન તેને ચૂકી જવું સહેલું છે: hook ની અંદર વપરાતા દરેક મૂલ્યને સામેલ કરો જે બદલાઈ શકે છે.

react-hooks/exhaustive-deps ESLint rule ચલાવો. તે સ્પષ્ટ ભૂલો પકડી લેશે. પરંતુ તેને રોબોટ તરીકે ન જુઓ. દરેક dependency શા માટે મહત્વની છે તે સમજો.

ઓવર-ઓપ્ટિમાઇઝેશનનો છુપો ટેક્સ

શરૂઆત કરનારાઓ ઘણીવાર દરેક function અને દરેક value ને આ hooks થી સુરક્ષિત રાખવાનો પ્રયત્ન કરે છે કારણ કે તે સુરક્ષિત લાગે છે. આ આદત ઉલટી પડે છે.

React એ cached values ને મેમરીમાં સ્ટોર કરવા પડે છે. દરેક render પર, તેણે તમારા dependency array માંથી પસાર થવું પડે છે અને Object.is નો ઉપયોગ કરીને દરેક આઇટમની સરખામણી કરવી પડે છે. તે સરખામણી સસ્તી છે, પરંતુ તે મફત નથી. જો તમે onClick={() => setOpen(true)} જેવા સામાન્ય event handler ને useCallback ની અંદર લપેટો છો, તો તમે એવું function બનાવવાનું ટાળવા માટે memory અને CPU નો ખર્ચ કરી રહ્યા છો જેનું એલોકેશન ત્વરિત હોત.

Hooks વધારાનો ઘોંઘાટ (noise) પણ ઉમેરે છે. useMemo અને useCallback માં લપેટાયેલ કોડ વાંચવામાં અને જાળવવામાં મુશ્કેલ હોય છે. દરેક dependency array એ એક સંભવિત stale closure છે જે તમને મુશ્કેલીમાં મૂકી શકે છે.

સાચો નિયમ આકર્ષક નથી પણ અસરકારક છે: પહેલા સાદો કોડ લખો. જ્યારે તમારી પાસે સમસ્યાનો પુરાવો હોય ત્યારે જ ઓપ્ટિમાઇઝ કરો. કયા components ખર્ચાળ છે અને કયા renders બિનજરૂરી છે તે ઓળખવા માટે React DevTools Profiler નો ઉપયોગ કરો. જો render માં થોડા મિલીસેકન્ડથી પણ ઓછો સમય લાગે છે, તો કોઈ યુઝર તેને નોંધશે નહીં, અને તમારું memoization કંઈ જ ઉકેલી રહ્યું નથી.

નિષ્કર્ષ

useMemo ખર્ચાળ values માટે છે. useCallback સ્થિર function references માટે છે. કોઈપણ hook તમારા component ને આપમેળે ઝડપથી render કરતું નથી; તેઓ બિનજરૂરી downstream કામ અટકાવે છે. તેમના વગર શરૂઆત કરો, વાસ્તવિક ટૂલિંગ સાથે માપો, અને જ્યાં profiler બોટલનેક (bottleneck) બતાવે છે ત્યાં જ તેમને ચોકસાઈથી ઉમેરો. ક્યારેક રિ-રેન્ડર થતો સાફ કોડ (Clean code) હંમેશા એવા ઓવર-એન્જિનિયર્ડ કોડ કરતા વધુ સારો રહેશે જે બધું જ memoize કરે છે.