📥 L'utilità della Memoization in JavaScript: Come ottimizzare le performance delle tue funzioni

 


Quando si lavora con funzioni complesse o ripetitive in JavaScript, ottimizzare le prestazioni diventa fondamentale. In questo articolo scoprirai cos’è la memoization, come funziona e perché è una tecnica così efficace per migliorare la velocità delle tue applicazioni.

🔗 Ti piace Techelopment? Dai un'occhiata al sito per tutti i dettagli!

Cos'è la Memoization?

La memoization è una tecnica di ottimizzazione che consiste nel memorizzare i risultati di chiamate a funzione costose, in modo che successive invocazioni con gli stessi argomenti possano restituire immediatamente il valore precedentemente calcolato, evitando ricalcoli inutili.

In pratica, memoizzare una funzione significa aggiungere una cache che salva i risultati in base ai parametri ricevuti. Se una funzione viene chiamata più volte con gli stessi argomenti, si legge il valore dalla cache anziché rieseguire l'intera funzione.


Perché Usare la Memoization?

Le principali situazioni in cui la memoization è utile includono:

  • Calcoli matematici ricorsivi pesanti (es. Fibonacci, fattoriali).
  • Funzioni pure con input limitati e ripetuti.
  • Rendering complessi in ambienti React o animazioni.
  • Trasformazioni su dati immutabili o grandi dataset.

Benefici chiave:

  • Prestazioni migliorate: Riduce il numero di esecuzioni ridondanti.
  • Risposta immediata: Migliora la reattività delle applicazioni.
  • Riduzione del carico: Ottimizza l’uso della CPU, utile per funzioni CPU-bound.
  • Ideale per funzioni deterministiche: Se una funzione restituisce sempre lo stesso output per un dato input, memoizzarla è sicuro ed efficace.

Come Funziona la Memoization in JavaScript?

Vediamo un esempio pratico di funzione senza memoization e poi una versione ottimizzata.

Funzione senza memoization

function slowFibonacci(n) {
  if (n <= 1) return n;
  return slowFibonacci(n - 1) + slowFibonacci(n - 2);
}

console.log(slowFibonacci(40)); // Estremamente lento

Questa funzione è un classico esempio di ricorsione esponenziale, dove ogni chiamata genera due nuove chiamate. Il tempo di esecuzione cresce molto rapidamente.

Versione memoizzata

function memoizedFibonacci() {
  const cache = {};

  return function fib(n) {
    if (n in cache) {
      return cache[n];
    }
    if (n <= 1) return n;

    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  };
}

const fibonacci = memoizedFibonacci();

console.log(fibonacci(40)); // Molto più veloce

In questa versione, la funzione fib salva i risultati in una cache (cache[n]). Ogni risultato viene calcolato una sola volta, poi riutilizzato.


Esempio di Memoization con chiamate API

La memoization può essere utile anche per evitare chiamate API ridondanti. Se una determinata richiesta API restituisce sempre lo stesso risultato per gli stessi parametri, possiamo salvare il risultato nella cache e riutilizzarlo nelle chiamate successive.

function memoizeApiCall(apiFunction) {
  const cache = new Map();

  return async function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key); // ritorna dalla cache
    }

    const result = await apiFunction(...args);
    cache.set(key, result);
    return result;
  };
}

// Esempio di chiamata API
async function fetchUserData(userId) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  if (!response.ok) throw new Error("Errore nella richiesta");
  return response.json();
}

// Versione memoizzata
const memoizedFetchUserData = memoizeApiCall(fetchUserData);

// Uso
memoizedFetchUserData(1).then(data => console.log("Prima chiamata:", data));
memoizedFetchUserData(1).then(data => console.log("Seconda chiamata dalla cache:", data));

In questo esempio, memoizeApiCall avvolge una funzione API asincrona e salva il risultato per ogni set di parametri (qui, l'userId). La seconda chiamata con lo stesso ID non genera una nuova richiesta HTTP, ma restituisce subito il risultato dalla cache.

⚠️ Nota: La cache è in memoria, quindi si resetta al reload della pagina. Per cache persistenti potresti considerare localStorage o IndexedDB.


Implementare una Funzione Generica di Memoization

La memoization può essere facilmente generalizzata in JavaScript per qualsiasi funzione con input deterministici:

function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Esempio d'uso
function add(a, b) {
  console.log("Computing...");
  return a + b;
}

const memoizedAdd = memoize(add);

console.log(memoizedAdd(3, 4)); // Computing... 7
console.log(memoizedAdd(3, 4)); // From cache: 7

Questa versione usa Map e JSON.stringify per creare una chiave unica basata sugli argomenti. È una tecnica molto comune e riutilizzabile.


Memoization e Funzioni Non Pure

⚠️ È importante ricordare che la memoization funziona solo con funzioni pure, ovvero:

  • Stesso output per stessi input.
  • Nessun effetto collaterale (side effect).

Funzioni che dipendono da stato globale, input/output, o interazioni con l’ambiente non sono adatte alla memoization, perché il caching potrebbe produrre risultati non aggiornati o errati.


Memoization in ambienti moderni (React, Lodash, etc.)

In React

Nel mondo React, memoization è usata per:

  • Evitare ricalcoli costosi (con useMemo).
  • Evitare ricostruzione di funzioni (con useCallback).
  • Evitare rendering inutili (con React.memo).
const memoizedValue = useMemo(() => expensiveFunction(input), [input]);

Lodash

La libreria Lodash include una funzione _.memoize():

const _ = require('lodash');

const memoizedFn = _.memoize(someExpensiveFunction);

Lodash gestisce internamente la cache ed è utile per applicazioni più complesse o produzione.


Limiti e Considerazioni

  • Uso della memoria: Ogni risultato è memorizzato; se gli input sono molti o unici, la cache può crescere molto.
  • Cache invalidation: Non è facile gestire quando “scadere” o aggiornare la cache.
  • Argomenti complessi: JSON.stringify ha limiti con oggetti ciclici o funzioni come argomenti.
  • Spazio/tempo trade-off: Aumenti di prestazioni a costo di uso di memoria.

Conclusioni

La memoization è una potente tecnica di ottimizzazione per le funzioni deterministiche, specialmente quelle costose da eseguire ripetutamente. In JavaScript, è semplice da implementare ed è molto utile in applicazioni web moderne, soprattutto se combinate con React o librerie come Lodash.

💡 Best Practice: Memoizza solo funzioni pure, usa strumenti pronti quando disponibili (React, Lodash), e tieni d’occhio la gestione della cache.



Follow me #techelopment

Official site: www.techelopment.it
facebook: Techelopment
instagram: @techelopment
X: techelopment
Bluesky: @techelopment
telegram: @techelopment_channel
whatsapp: Techelopment
youtube: @techelopment