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:
- Determinística: Para os mesmos inputs, sempre retorna o mesmo output.
- Sem efeitos colaterais: Não modifica nenhum estado fora de seu escopo local.
- 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?
Fonte: https://www.andrealtoe.me/