Padrões Práticos de Memoization em JavaScript
Você perfilou sua aplicação e encontrou uma função sendo executada milhares de vezes com entradas idênticas. Memoization parece ser a solução óbvia. Mas antes de envolver tudo em um cache, você deve saber: memoization feita de forma incorreta cria bugs mais difíceis de encontrar do que os problemas de desempenho com os quais você começou.
Este artigo cobre padrões práticos de memoization em JavaScript, armadilhas comuns de memoization que desenvolvedores JavaScript encontram, e como aplicar essas técnicas com segurança em código de produção—incluindo memoization assíncrona e melhores práticas de useMemo do React.
Principais Conclusões
- Memoization armazena em cache os resultados de funções com base em argumentos, trocando memória por velocidade—mas só funciona de forma confiável com funções puras e argumentos primitivos.
- Referências de objetos causam acertos de cache obsoletos quando os dados mudam; use bibliotecas como fast-memoize para chaveamento personalizado ou mantenha-se com primitivos.
- Caches ilimitados causam vazamento de memória em aplicações de longa execução; implemente remoção LRU, expiração TTL, ou escopo de caches para ciclos de vida de componentes.
- Memoization assíncrona requer armazenar promises imediatamente em cache e deletar entradas com falha para prevenir requisições duplicadas e permitir novas tentativas.
- O useMemo do React é uma otimização direcionada, não um padrão—perfile primeiro e só aplique quando computações forem mensuravelmente lentas.
O Que Memoization Realmente Faz
Memoization armazena em cache os resultados de funções com base em seus argumentos. Chame a função novamente com as mesmas entradas, e você obtém o resultado em cache ao invés de recomputar.
JavaScript não possui memoization nativa. O TC39 discutiu propostas (por exemplo, Function.prototype.memo), mas nada está pronto para produção ainda (proposta). Você precisará implementar isso você mesmo ou usar uma biblioteca.
Aqui está um padrão básico para funções de argumento único:
function memoize(fn) {
const cache = new Map()
return (...args) => {
const key = args[0]
if (cache.has(key)) return cache.get(key)
const result = fn(...args)
cache.set(key, result)
return result
}
}
Isso funciona para argumentos primitivos. Quebra de formas sutis para todo o resto.
O Problema de Referência de Objetos
Objetos são armazenados em cache por referência, não por valor. Isso pega desenvolvedores constantemente:
const memoizedFn = memoize(processData)
const config = { threshold: 10 }
memoizedFn(config) // Computa
config.threshold = 20
memoizedFn(config) // Retorna resultado em cache obsoleto
Mesma referência significa acerto de cache, mesmo que os dados tenham mudado.
Alguns desenvolvedores tentam corrigir isso usando JSON.stringify(args) como chave de cache. Isso pode funcionar para dados simples, mas falha em referências circulares, descarta funções e símbolos, e pode ser lento para objetos grandes.
A solução: Apenas memoize funções com argumentos primitivos, ou use uma biblioteca como fast-memoize que suporta resolvedores/serializadores de chave personalizados para casos mais complexos.
Quando Memoization Causa Problemas
Funções Impuras
Memoizar funções impuras cria problemas impossíveis de depurar:
// Nunca memoize isso para cache de longo prazo
const getData = memoize(() => {
return fetch('/api/data').then(r => r.json())
})
A primeira chamada armazena a promise em cache. Toda chamada subsequente retorna a mesma promise—mesmo se os dados do servidor mudaram.
Memoization assíncrona só é segura quando você intencionalmente quer deduplicar requisições concorrentes, ou quando você também implementa invalidação ou expiração baseada em TTL (coberto abaixo).
Crescimento Ilimitado de Cache
Sem estratégias de remoção de cache, seu cache cresce para sempre:
// Vazamento de memória esperando para acontecer
const processUserInput = memoize((input) => expensiveOperation(input))
Toda entrada única adiciona ao cache. Em uma aplicação de longa execução, isso causa vazamento de memória.
Soluções:
- Defina um tamanho máximo de cache (remoção LRU)
- Adicione expiração TTL (time-to-live)
- Escopo de caches para ciclos de vida de componentes
function memoizeWithLimit(fn, maxSize = 100) {
const cache = new Map()
return (...args) => {
const key = args[0]
if (cache.has(key)) return cache.get(key)
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value
cache.delete(firstKey)
}
const result = fn(...args)
cache.set(key, result)
return result
}
}
Discover how at OpenReplay.com.
Memoization Assíncrona Feita Corretamente
Memoization assíncrona precisa de tratamento especial para chamadas concorrentes e falhas:
function memoizeAsync(fn) {
const cache = new Map()
return async (...args) => {
const key = args[0]
if (cache.has(key)) return cache.get(key)
const promise = fn(...args).catch(err => {
cache.delete(key) // Não armazene falhas em cache
throw err
})
cache.set(key, promise)
return promise
}
}
Armazene a promise imediatamente em cache. Isso previne requisições concorrentes duplicadas. Delete em caso de falha para que novas tentativas funcionem.
Melhores Práticas de useMemo do React
useMemo e React.memo são otimizações direcionadas, não padrões. Eles adicionam complexidade e podem prejudicar o desempenho quando mal utilizados (veja a documentação oficial do React: https://react.dev/reference/react/useMemo).
Use useMemo quando:
- Computar dados derivados de props/state é mensuravelmente lento
- Você está passando objetos para filhos memoizados
Pule useMemo quando:
- A computação é trivial
- Você não mediu um problema de desempenho
// Provavelmente desnecessário
const doubled = useMemo(() => value * 2, [value])
// Potencialmente útil
const sortedItems = useMemo(
() => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
[items]
)
A comparação de dependências do React usa Object.is—igualdade de referência. Novos objetos literais quebram a memoization a cada renderização.
O Framework de Decisão
Antes de memoizar, pergunte:
- A função é pura? Sem efeitos colaterais, mesmas entradas sempre produzem mesmas saídas.
- É realmente lenta? Perfile primeiro. Não adivinhe.
- Os argumentos são primitivos ou referências estáveis? Argumentos de objeto precisam de tratamento cuidadoso.
- Qual é o tempo de vida do cache? Caches ilimitados causam vazamento de memória.
Conclusão
Memoization troca memória por velocidade. Certifique-se de que está fazendo um bom negócio. Comece perfilando para confirmar que um problema real de desempenho existe, então aplique memoization seletivamente a funções puras com argumentos previsíveis. Implemente limites de cache para prevenir vazamentos de memória, trate operações assíncronas cuidadosamente, e lembre-se que o useMemo do React é uma ferramenta de otimização—não um padrão. Quando feita corretamente, memoization elimina computação redundante. Quando feita incorretamente, introduz bugs sutis que duram mais que os ganhos de desempenho.
Perguntas Frequentes
Memoizar chamadas de API é arriscado porque os dados do servidor mudam ao longo do tempo. Se você armazena a promise em cache, chamadas subsequentes retornam dados obsoletos. Só memoize chamadas de API quando você explicitamente quer deduplicar requisições concorrentes e você implementa invalidação de cache ou expiração TTL para atualizar dados periodicamente.
Objetos são comparados por referência, não por valor. Se você muta um objeto e chama a função memoizada novamente, ela retorna o resultado em cache porque a referência não mudou. Use padrões de dados imutáveis, crie novos objetos ao invés de mutar, ou serialize argumentos com JSON.stringify para casos simples.
Perfile antes e depois usando DevTools do navegador ou ferramentas de profiling do Node.js. Meça tempo de execução e uso de memória. Se a função executa raramente ou computa rapidamente, o overhead de memoization pode exceder os ganhos. Taxa de acerto de cache também importa—taxas baixas significam memória desperdiçada com benefício mínimo.
Não. useMemo adiciona overhead para rastreamento e comparação de dependências. Para cálculos simples como matemática básica ou concatenação de strings, o custo de memoization excede o custo de computação. Reserve useMemo para operações caras como ordenar arrays grandes, filtragem complexa, ou criar objetos passados para componentes filhos memoizados.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.