பெரும்பாலான React செயல்திறன் (performance) பயிற்சிகள் ஒரே தவறான ஆலோசனையுடன் முடிகின்றன: அனைத்தையும் useMemo மற்றும் useCallback-க்குள் வைத்துவிட்டு வேலையை முடித்துவிடலாம் என்பதுதான். நீங்கள் அந்த ஆலோசனையைப் பின்பற்றியிருந்தால், உங்கள் செயலியை (application) இன்னும் மெதுவாக்கியிருக்கலாம். இந்த hooks இலவசமானவை அல்ல. ஒவ்வொன்றும் நினைவகத்தை (memory) ஒதுக்குகிறது, சார்புகளை (dependencies) ஒப்பிடுகிறது மற்றும் சேமிக்கப்பட்ட மதிப்புகளை (cached values) சேமிக்கிறது. நோக்கமின்றிப் பயன்படுத்தும்போது, அவை மேம்படுத்தலுக்குப் பதிலாக கூடுதல் சுமையாக (overhead) மாறுகின்றன.

உண்மையில் எது முக்கியம் என்பதை மட்டும் பார்ப்போம்.

ஒவ்வொரு Hook-உம் உண்மையில் என்ன செய்கிறது

useMemo ஒரு மதிப்பை (value) நினைவில் வைத்துக்கொள்ளும். நீங்கள் கடினமான வேலையைச் செய்யும் ஒரு செயல்பாட்டை (function) அதற்கு வழங்கினால், அது அதன் முடிவைத் திருப்பித் தரும். அடுத்த render-ன் போது, உங்கள் dependencies மாறவில்லை என்றால், React அந்த கணக்கீட்டைத் தவிர்த்துவிட்டு பழைய முடிவைத் தரும்.

useCallback ஒரு செயல்பாட்டை (function) நினைவில் வைத்துக்கொள்ளும். இது உங்களுக்காக அந்தச் செயல்பாட்டை இயக்காது. அதன் dependencies மாறாத வரை, render-களுக்கு இடையில் அதே function instance-ஐ இது திருப்பித் தரும்.

இதுதான் முழுமையான வித்தியாசம். ஒன்று கணக்கிடப்பட்ட மதிப்பை (computed value) சேமிக்கிறது. மற்றொன்று ஒரு குறிப்பினை (reference) சேமிக்கிறது. இவற்றைத் தவறாகப் பயன்படுத்துவது, பார்ப்பதற்கு மேம்படுத்தப்பட்டது போலத் தோன்றும் ஆனால் கூடுதல் நினைவகச் செலவைச் செய்யும் அதே வேளையில், சாதாரண குறியீட்டைப் போலவே செயல்படும்.

Function Identity ஏன் உங்கள் Tree-ஐப் பாதிக்கிறது

ஒரு component மறுமுறை render செய்யப்படும்போது (re-renders), React முழு function body-யையும் மீண்டும் இயக்கும். ஒவ்வொரு மாறியும் (variable) மீண்டும் உருவாக்கப்படும். ஒவ்வொரு inline function-க்கும் நினைவகத்தில் ஒரு புதிய முகவரி (address) கிடைக்கும்.

JavaScript-இல், ஒரே மாதிரியான தர்க்கத்தைக் (logic) கொண்ட இரண்டு functions சமமானவை அல்ல. () => {} === () => {} என்பது false என்று வரும். இதே விதி objects மற்றும் arrays-களுக்கும் பொருந்தும். உங்கள் parent component handleSubmit-ஐ வரையறுத்து அதை ஒரு child-க்கு அனுப்பினால், ஒவ்வொரு render-ன் போதும் அந்த child ஒரு புதிய prop-ஐப் பெறும். அந்த child React.memo-வில் சுற்றப்பட்டிருந்தாலும் கூட, புதிய function பழையது போலவே செயல்படுகிறது என்பதை அதனால் கண்டறிய முடியாது. reference மாறியதால், child component மறுமுறை render ஆகிறது.

இதுதான் useCallback தீர்ப்பதற்காக உருவாக்கப்பட்ட அடிப்படைப் பிரச்சனை. இது வேகத்தைப் பற்றியது அல்ல; இது நிலைத்தன்மையைப் (stability) பற்றியது.

useMemo எப்போது பயனுள்ளதாக இருக்கும்

நீங்கள் செய்யும் வேலை உண்மையில் அதிகச் சுமையைத் தரும்போது மற்றும் அதனால் ஒரு அளவிடக்கூடிய தாமதம் (lag) ஏற்படுவதைக் காணும்போது, உங்களுக்கு useMemo தேவைப்படும்.

ஒரு மிகப்பெரிய தரவுத் தொகுப்பை (dataset) filter செய்வதைப் பற்றிச் சிந்திப்போம். பல்லாயிரக்கணக்கான வரிசைகளைக் கொண்ட ஒரு அட்டவணை மற்றும் ஒரு search input இருந்தால், உங்கள் component-க்குள் இப்படி எழுதலாம்:

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

useMemo இல்லையென்றால், அந்த loop ஒவ்வொரு render-ன் போதும் இயங்கும். பயனர் ஒரு sidebar-ஐத் திறக்கும்/மூடும் button-ஐ அழுத்தினால், parent component மறுமுறை render ஆகும், இதனால் rows மற்றும் query மாறாத போதும் உங்கள் filter மீண்டும் இயங்கும். ஒரு பெரிய dataset-இல், அந்தத் தடுமாற்றம் (stutter) தெளிவாகத் தெரியும்.

useMemo முடிவை நிலைநிறுத்துவதன் மூலம் இதைச் சரிசெய்கிறது:

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

இப்போது dependencies உண்மையில் மாறும்போது மட்டுமே React அந்த filter-ஐ மீண்டும் இயக்கும்.

இதே தர்க்கம் சிக்கலான கணிதக் கணக்கீடுகள், API responses-களை chart-friendly வடிவங்களாக மாற்றுவது அல்லது தொடர்ந்து மறு கணக்கீடு செய்யப்பட வேண்டிய state-களை உருவாக்குவது போன்றவற்றுக்கும் பொருந்தும்.

இரண்டாவது, அவ்வளவு எளிதில் தெரியாத ஒரு பயன்பாடு உள்ளது. நீங்கள் ஒரு object அல்லது array-ஐ உள்ளூர் ரீதியாக (locally) உருவாக்கி அதை useEffect dependency array-இல் சேர்த்தால், ஒவ்வொரு render-ன் போதும் அந்த effect தற்செயலாகத் தூண்டப்படலாம். Inline objects மற்றும் arrays ஒவ்வொரு முறையும் புதிய அடையாளங்களைப் (identities) பெறுவதால், மாற்றப்பட்ட dependency-யைக் கண்டு effect மீண்டும் இயங்கும். useMemo மூலம் அந்த object-ஐ memoize செய்வது reference-ஐ நிலையாக வைத்திருக்கும், இதனால் அடிப்படைத் தரவு உண்மையில் மாறும்போது மட்டுமே உங்கள் effect இயங்கும்.

useCallback எப்போது அவசியமாகிறது

React.memo-ஆல் மேம்படுத்தப்பட்ட child components-களுக்கு handlers-களை அனுப்பும்போது useCallback மிகவும் முக்கியமானது.

ஒரு counter-ஐக் கொண்ட parent component-ஐக் கற்பனை செய்து பாருங்கள். அது ஒரு அதிகச் சுமையுள்ள (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 மறுமுறை render ஆகும். ஒரு புதிய handleItemClick உருவாக்கப்படும். ExpensiveList ஒரு புதிய prop reference-ஐப் பெறுவதால், அதுவும் மறுமுறை render ஆகிறது. ExpensiveList React.memo-வில் சுற்றப்பட்டிருந்தாலும் கூட, function prop மாறியதால் அந்த memoization முற்றிலும் வீணாகிவிடும்.

useCallback அந்த reference-ஐப் பாதுகாக்கிறது:

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

இப்போது ExpensiveList உண்மையில் தேவைப்படும்போது மட்டுமே மறுமுறை render ஆகும்.

மற்றொரு முக்கியமான சூழல் useEffect-ஐ உள்ளடக்கியது. ஒரு effect உங்கள் component-க்குள் வரையறுக்கப்பட்ட ஒரு function-ஐப் பயன்படுத்துகிறது (subscribes) என்றால், அந்த function ஒவ்வொரு render-ன் போதும் அதன் அடையாளத்தை மாற்றினால், அந்த effect மீண்டும் மீண்டும் நிறுத்தப்பட்டு (teardown) மீண்டும் இணைக்கப்படும் (resubscribe). அந்த function-ஐ memoize செய்வது effect-ஐ நிலையாக வைத்திருக்கும்.

Dependency Array பொறி மற்றும் Stale Closures

இரண்டு hooks-களும் dependency arrays-களைச் சார்ந்தே உள்ளன, பெரும்பாலான பிழைகள் (bugs) இங்கேயே ஒளிந்துள்ளன.

நீங்கள் dependency array-லிருந்து ஒரு மாறியை (variable) நீக்கினால், உங்கள் memoized function அல்லது மதிப்பு அந்த மாறியின் பழைய பதிப்பையே எடுத்துக்கொள்ளும். இது ஒரு stale closure ஆகும். UI புதிய தரவைக் காட்டலாம், ஆனால் உங்கள் callback இன்னும் மூன்று renders முன்னால் இருந்த state-ஐயே பார்க்கக்கூடும். இதற்கான தீர்வு எளிமையானது, ஆனால் code reviews செய்யும்போது கவனிக்கத் தவறிவிடலாம்: hook-க்குள் பயன்படுத்தப்படும், மாறக்கூடிய ஒவ்வொரு மதிப்பையும் அதில் சேர்க்கவும்.

react-hooks/exhaustive-deps ESLint rule-ஐ இயக்கவும். இது வெளிப்படையான விடுபடல்களைக் கண்டறியும். ஆனால் அதை ஒரு ரோபோவாகக் கருத வேண்டாம். ஒவ்வொரு dependency-யும் ஏன் முக்கியமானது என்பதைப் புரிந்து கொள்ளுங்கள்.

அதிகப்படியான Optimization-ன் மறைமுகத் தாக்கம்

ஆரம்பநிலைப் பயனர்கள் (Beginners) பாதுகாப்பானது என்று கருதி, ஒவ்வொரு function மற்றும் ஒவ்வொரு மதிப்பையும் இந்த hooks கொண்டு மூடிவிடுகிறார்கள். அந்தப் பழக்கம் எதிர்மறையான விளைவுகளை ஏற்படுத்தும்.

React, cached மதிப்புகளை memory-யில் சேமிக்க வேண்டும். ஒவ்வொரு render செய்யும் போதும், அது உங்கள் dependency array-யைச் சுழற்சி செய்து (iterate), ஒவ்வொரு மதிப்பையும் Object.is மூலம் ஒப்பிட வேண்டும். அந்த ஒப்பீடு எளிதானதுதான், ஆனால் அது முற்றிலும் இலவசமானது அல்ல. onClick={() => setOpen(true)} போன்ற ஒரு சாதாரண event handler-ஐ useCallback-க்குள் நீங்கள் போர்த்தினால், மிக எளிதாக உருவாக்கக்கூடிய ஒரு function-ஐத் தவிர்ப்பதற்காக, நீங்கள் memory மற்றும் CPU செலவுகளைச் செய்கிறீர்கள் என்று அர்த்தம்.

இந்த hooks தேவையற்ற குழப்பங்களையும் (noise) சேர்க்கின்றன. useMemo மற்றும் useCallback-க்குள் இருக்கும் code-ஐப் படிப்பது மற்றும் பராமரிப்பது கடினம். ஒவ்வொரு dependency array-யும் உங்களைத் தாக்கக் காத்திருக்கும் ஒரு சாத்தியமான stale closure ஆகும்.

உண்மையான அடிப்படை விதி கவர்ச்சியற்றது ஆனால் பயனுள்ளது: முதலில் சாதாரண code-ஐ எழுதுங்கள். ஒரு பிரச்சனை இருப்பதை உறுதி செய்த பின்னரே optimize செய்யுங்கள். எந்த components அதிகச் செலவைச் செய்கின்றன மற்றும் எந்த renders வீணாகின்றன என்பதைக் கண்டறிய React DevTools Profiler-ஐப் பயன்படுத்தவும். ஒரு render சில மில்லி விநாடிகளுக்கும் குறைவாகவே எடுத்துக் கொண்டால், எந்தப் பயனரும் அதை உணர மாட்டார்கள், மேலும் உங்கள் memoization எதையும் தீர்க்கவில்லை என்று அர்த்தம்.

சுருக்கமாகச் சொன்னால்

useMemo என்பது அதிகச் செலவு பிடிக்கும் மதிப்புகளுக்கானது (expensive values). useCallback என்பது நிலையான function references-க்கானது. இந்த hooks எதுவும் உங்கள் component-ஐ தானாகவே வேகமாக render செய்யாது; அவை தேவையற்ற அடுத்தகட்ட வேலைகளைத் (downstream work) தடுக்கின்றன. அவற்றை இன்றித் தொடங்குங்கள், முறையான கருவிகளைக் கொண்டு அளவிடுங்கள், மேலும் profiler எங்கே ஒரு தடையைக் (bottleneck) காட்டுகிறதோ அங்கேயே அவற்றைச் சேர்க்கவும். அவ்வப்போது rerender ஆகும் சுத்தமான code, அனைத்தையும் memoize செய்யும் அளவுக்கு அதிகப்படியான பொறியியல் நுணுக்கங்களைக் கொண்ட (over-engineered) code-ஐ விட எப்போதும் சிறந்தது.