Memoização: não é a técnica da sua avó para lembrar coisas.

Primeiramente, devo dizer que não está escrito errado! A memoização é uma técnica utilizada para agilizar a velocidade de funções criando um cache de resultados. É como dar à sua função um caderno para anotar as respostas e não ter de calcular as coisas novamente, inteligente, não é?

Um rápido adendo: funções puras

Uma função pura é uma função que possui as seguintes características:

  1. Determinística: Para os mesmos inputs, sempre retorna o mesmo output.
  2. Sem efeitos colaterais: Não modifica nenhum estado fora de seu escopo local.
  3. Não depende de estado externo: Seu resultado depende apenas dos argumentos passados.

Exemplo:

// Função não pura ❌
function add(){
  const a = prompt(`Numero 1:`);
  const b = prompt(`Numero 2:`);
  return a + b;
}

let total = 0;
// Função não pura ❌
function somaAoTotal(valor) {
  total += valor;
  return total;
}

// Função pura ✅
function add(a, b){
  return a + b;
}

Para realizar a memoização é imprescindível que ela seja feita com funções puras.

Memoizando o último valor calculado

Imagine que você tem uma aplicação de frontend com um botão que realiza um cálculo complexo que demora 200 ms para ser concluído. Caso o usuário clique desesperadamente neste botão o navegador simplesmente irá ficar por momentos travado, como podemos resolver um problema como este? Memoização

Para isso, vamos criar uma função utilitária para adicionar memoização à nossa função custosa

function memoizeLastResult(fn) {
  // Armazena os argumentos da última chamada
  let lastArgs = null;
  // Armazena o resultado da última chamada
  let lastResult = null;

  // Retorna uma nova função que envolve a função original
  return function(...args) {
    // Compara os argumentos atuais com os da última chamada
    // Usa JSON.stringify para comparar arrays/objetos profundamente
    if (JSON.stringify(args) === JSON.stringify(lastArgs)) {
      // Se os argumentos são iguais, retorna o resultado cacheado
      return lastResult;
    }
    
    // Se os argumentos são diferentes:
    // 1. Atualiza lastArgs com os argumentos atuais
    lastArgs = args;
    // 2. Chama a função original e armazena o resultado
    lastResult = fn.apply(this, args);
    // 3. Retorna o novo resultado
    return lastResult;
  };
}

// Uso:
// const memoizedFn = memoizeLastResult(originalFunction);
// memoizedFn() agora terá o comportamento de memoização

Assim, na próxima chamada da função memoizada o resultado virá instantâneamente com o último valor calculado.

Exemplo:

// Função que simula um cálculo custoso
function calcularAreaComplexa(largura, altura) {
  console.log(`Calculando área para ${largura} x ${altura}...`);
  
  // Simulando um cálculo demorado
  let resultado = 0;
  for (let i = 0; i < 1000000; i++) {
    resultado += largura * altura;
  }
  
  return resultado / 1000000;
}

// Aplicando memoização à função de cálculo
const calcularAreaMemoizada = memoizeLastResult(calcularAreaComplexa);

// Exemplos de uso
console.time("Primeira chamada");
console.log(calcularAreaMemoizada(5, 3));
console.timeEnd("Primeira chamada");

console.time("Segunda chamada (mesmos argumentos)");
console.log(calcularAreaMemoizada(5, 3));
console.timeEnd("Segunda chamada (mesmos argumentos)");

console.time("Terceira chamada (argumentos diferentes)");
console.log(calcularAreaMemoizada(6, 4));
console.timeEnd("Terceira chamada (argumentos diferentes)");

console.time("Quarta chamada (argumentos iguais aos da terceira)");
console.log(calcularAreaMemoizada(6, 4));
console.timeEnd("Quarta chamada (argumentos iguais aos da terceira)");

Neste exemplo temos o seguinte resultado

Calculando área para 5 x 3... 15 Primeira chamada: 2.713134765625 ms 15 Segunda chamada (mesmos argumentos): 0.02294921875 ms Calculando área para 6 x 4... 24 Terceira chamada (argumentos diferentes): 0.761962890625 ms 24 Quarta chamada (argumentos iguais aos da terceira): 0.017822265625 ms

Assim podemos observar que entre a primeira chamada e a segunda com os mesmos argumentos houve uma diminuição ABSURDA no tempo de resposta! Porém, ao trocar os argumentos, não haverá mais o benefício da memoização, sendo assim, uma técnica que não servirá tão bem a funções que são constantemente invocadas com diferentes parâmetros.

Memoizando todos valores calculados

Este método não apenas memoiza o último valor, mas, todos os resultados calculados utilizando de um map.

function memoizeAll(fn) {
  // Cria um Map para armazenar os resultados cacheados
  // Map é usado em vez de um objeto simples para melhor performance com chaves complexas
  const cache = new Map();

  // Retorna uma nova função que envolve a função original
  return function(...args) {
    // Converte os argumentos em uma string para usar como chave do cache
    // JSON.stringify é usado para lidar com argumentos que são objetos ou arrays
    const key = JSON.stringify(args);

    // Verifica se o resultado para estes argumentos já está no cache
    if (cache.has(key)) {
      // Se estiver no cache, retorna o resultado armazenado
      return cache.get(key);
    }

    // Se não estiver no cache, executa a função original
    // 'apply' é usado para preservar o contexto 'this' e passar os argumentos
    const result = fn.apply(this, args);

    // Armazena o resultado no cache para uso futuro
    cache.set(key, result);

    // Retorna o resultado calculado
    return result;
  };
}

// Uso:
// const memoizedFn = memoizeAll(originalFunction);
// memoizedFn() agora terá o comportamento de memoização completa

Neste caso haverá um aumento de performance em todas chamadas com argumentos repetidos, porém, conforme alteramos os argumentos da chamada à função memoizada nosso map continuará crescendo.

Portanto, esta solução troca um desempenho mais rápido por um crescimento de memória potencialmente ilimitado. Nos piores casos, isso pode resultar na falha da guia do navegador, especialmente se cada resultado usar uma parte significativa da memória (por exemplo, uma árvore DOM).


Além destas formas, você pode também implementar algo como memoização apenas nos últimos N resultados para que não haja um crescimento ilimitado na memória utilizada ou até mesmo utilizar de um WeakMap que poderá ser automaticamente limpo sempre que o objeto chave não existir mais.

Por fim, a memoização é uma técnica para que seu programa se torne mais performático utilizando de cache em funções específicas que são chamadas com uma elevada frequência e cabe a você entender onde ele poderá ou não ser utilizado.

E você, já conhecia essa técnica? Já viu sua utilização em algum lugar além do famigerado memo/useMemo do React?

Legal o artigo man! Conheci o conceito tempos atrás através da biblioteca Guava (Java). Eles tem o Suppliers.memoize que serve basicamente pra isso. Usei num cenário onde uma prop poderia ser calculada 0, 1 ou N vezes. E a lib ajudava pq além de tudo se torna algo "lazy", i.e., nos cenários onde não precisava computar não tinha o trabalho adicional pro processador.

Complmentos:

S2


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).