La maggior parte dei tutorial sulle performance di React termina con lo stesso cattivo consiglio: avvolgi tutto in useMemo e useCallback e considera il lavoro finito. Se hai seguito questo consiglio, probabilmente hai reso la tua applicazione più lenta. Questi hook non sono gratuiti. Ognuno di essi alloca memoria, confronta le dipendenze e memorizza valori in cache. Se usati senza uno scopo, diventano un sovraccarico invece di un'ottimizzazione.
Andiamo al sodo e concentriamoci su ciò che conta davvero.
Cosa fa realmente ogni hook
useMemo ricorda un valore. Gli passi una funzione che esegue un lavoro pesante e lui restituisce il risultato. Al render successivo, se le tue dipendenze non sono cambiate, React salta il calcolo e restituisce il vecchio risultato.
useCallback ricorda una funzione. Non esegue la funzione per te. Restituisce semplicemente la stessa istanza della funzione tra i render, finché le sue dipendenze rimangono invariate.
Questa è tutta la differenza. Uno memorizza un valore calcolato. L'altro memorizza un riferimento. Confonderli porta a un codice che sembra ottimizzato ma si comporta esattamente come il codice non avvolto, pur comportando un consumo extra di memoria.
Perché l'identità delle funzioni compromette il tuo albero dei componenti
Quando un componente viene renderizzato nuovamente, React esegue di nuovo l'intero corpo della funzione. Ogni variabile viene ricreata. Ogni funzione inline riceve un nuovo indirizzo in memoria.
In JavaScript, due funzioni che contengono esattamente la stessa logica non sono uguali. () => {} === () => {} restituisce false. La stessa regola si applica a oggetti e array. Se il tuo componente genitore definisce handleSubmit e la passa a un componente figlio, quel figlio riceverà una nuova prop a ogni singolo render. Anche se il figlio è avvolto in React.memo, non può capire che la nuova funzione fa la stessa cosa della vecchia. Il riferimento è cambiato, quindi il figlio viene renderizzato nuovamente.
Questo è il problema fondamentale che useCallback è stato creato per risolvere. Non si tratta di velocità. Si tratta di stabilità.
Quando useMemo ne vale la pena
Hai bisogno di useMemo quando stai eseguendo un'operazione oggettivamente costosa e puoi percepire un ritardo misurabile.
Pensa al filtraggio di un set di dati massiccio. Se hai una tabella con decine di migliaia di righe e un input di ricerca, potresti scrivere qualcosa del genere all'interno del tuo componente:
const visibleRows = rows.filter(r => r.name.includes(query));
Senza useMemo, quel ciclo viene eseguito a ogni render. Se l'utente clicca un pulsante che apre o chiude una barra laterale, il genitore viene renderizzato nuovamente e il tuo filtro viene eseguito di nuovo, anche se rows e query non sono mai cambiati. Su un set di dati di grandi dimensioni, quel rallentamento è visibile.
useMemo risolve il problema fissando il risultato:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
Ora React riesegue quel filtro solo quando le dipendenze cambiano effettivamente.
La stessa logica si applica a calcoli matematici complessi, alla trasformazione di risposte API in formati adatti ai grafici o alla derivazione di uno stato che altrimenti verrebbe ricalcolato costantemente.
C'è un secondo caso d'uso, meno ovvio. Se crei un oggetto o un array localmente e lo includi in un array di dipendenze di useEffect, potresti inavvertitamente attivare quell'effetto a ogni render. Gli oggetti e gli array inline ottengono nuove identità ogni volta, quindi l'effetto rileva una dipendenza cambiata e viene eseguito di nuovo. Memorizzare quell'oggetto con useMemo mantiene il riferimento stabile e permette al tuo effetto di essere eseguito solo quando i dati sottostanti cambiano effettivamente.
Quando useCallback diventa necessario
useCallback è fondamentale soprattutto quando passi degli handler a componenti figli che sono ottimizzati con React.memo.
Immagina un componente genitore che contiene un contatore. Esso renderizza anche una lista di figli costosa:
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>
);
}
Ogni volta che count cambia, Parent viene renderizzato nuovamente. Viene creata una nuova handleItemClick. Poiché ExpensiveList riceve un nuovo riferimento alla prop, viene renderizzata nuovamente anche lei. Se ExpensiveList è avvolta in React.memo, quella memorizzazione è completamente sprecata perché la prop della funzione è cambiata.
useCallback preserva il riferimento:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
Ora ExpensiveList viene renderizzata nuovamente solo quando è veramente necessario.
Un'altra situazione critica riguarda useEffect. Se un effetto si iscrive a una funzione definita all'interno del tuo componente e quella funzione cambia identità a ogni render, l'effetto verrà rimosso e reinscritto ripetutamente. Memorizzare la funzione mantiene l'effetto stabile.
La trappola dell'array di dipendenze e le stale closures
Entrambi gli hook si affidano agli array di dipendenze, ed è qui che si nascondono la maggior parte dei bug.
If you omit a variable from the dependency array, your memoized function or value closes over an old version of that variable. This is a stale closure. The UI might display fresh data, but your callback is still looking at state from three renders ago. The fix is simple but easy to miss during code reviews: include every value used inside the hook that could change.
Run the react-hooks/exhaustive-deps ESLint rule. It will catch obvious omissions. But do not treat it as a robot. Understand why each dependency matters.
The Hidden Tax of Over-Optimization
Beginners often armor every function and every value with these hooks because it feels safe. That habit backfires.
React must store cached values in memory. On every render, it must iterate through your dependency array and compare each item using Object.is. That comparison is cheap, but it is not free. If you wrap a trivial event handler like onClick={() => setOpen(true)} inside useCallback, you are paying memory and CPU costs to avoid creating a function that would have been instant to allocate.
The hooks also add noise. Code wrapped in useMemo and useCallback is harder to read and harder to maintain. Every dependency array is a potential stale closure waiting to bite you.
The real rule of thumb is unglamorous but effective: write plain code first. Optimize only when you have proof of a problem. Use the React DevTools Profiler to identify which components are expensive and which renders are wasteful. If a render takes less than a few milliseconds, no user will notice, and your memoization is solving nothing.
The Bottom Line
useMemo is for expensive values. useCallback is for stable function references. Neither hook makes your component render faster on its own; they prevent unnecessary downstream work. Start without them, measure with actual tooling, and add them precisely where the profiler shows a bottleneck. Clean code that rerenders occasionally will almost always beat over-engineered code that memoizes everything.
