Introdução: Por que o desempenho é importante
Na web moderna, a velocidade de uma aplicação afeta diretamente a experiência do usuário, a conversão e o ranking de SEO. Para aplicações server-side em Node.js, o desempenho é crítico: uma resposta lenta pode levar à perda de clientes e ao aumento dos custos de infraestrutura. Neste artigo, abordaremos os principais métodos de otimização de código JavaScript que ajudarão você a tornar sua aplicação mais rápida e eficiente.
1. Profiling e identificação de gargalos
Antes de otimizar, é preciso entender o que está deixando a aplicação lenta. Use as ferramentas nativas do Node.js:
- Profiler nativo — execute a aplicação com a flag
--inspecte use o Chrome DevTools. - clinic.js — biblioteca de terceiros para análise visual de desempenho.
- console.time() — uma maneira rápida de medir a execução de trechos específicos de código.
Exemplo de uso do console.time():
console.time('dbQuery');const result = await database.find({ status: 'active' });console.timeEnd('dbQuery'); // Exibe o tempo de execução em milissegundosApós o profiling, você verá quais funções consomem mais tempo e poderá otimizá-las de forma direcionada.
2. Otimização do trabalho com assincronia e Event Loop
Node.js é construído sobre um event loop single-thread. Operações bloqueantes podem parar completamente o processamento de requisições. Aqui estão as regras principais:
- Evite versões síncronas de métodos (ex.:
fs.readFileSync) em produção — elas bloqueiam o event loop. - Use Promise.all() para executar tarefas assíncronas independentes em paralelo.
- Divida tarefas síncronas pesadas com
setImmediate()ouworker_threads.
Exemplo de processamento paralelo:
const [users, posts, comments] = await Promise.all([ db.getUsers(), db.getPosts(), db.getComments()]);Este código executa todas as três consultas simultaneamente, reduzindo o tempo total de espera.
3. Cache e memoização
Cache é uma das maneiras mais eficazes de acelerar uma aplicação. Para Node.js, as abordagens populares são:
- Cache in-memory — use
Mapou bibliotecas comonode-cache. - Cache distribuído — Redis ou Memcached para aplicações escaláveis.
- Memoização de funções — cache dos resultados de chamadas de função para os mesmos argumentos.
Exemplo de memoização simples:
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; };}
const expensiveCalculation = memoize((n) => { // Matemática pesada return n * 42;});Em chamadas repetidas com os mesmos argumentos, o resultado é obtido do cache, economizando tempo de CPU.
4. Redução da carga no Garbage Collector
O Garbage Collector (GC) no V8 é uma ferramenta poderosa, mas suas ativações frequentes podem causar "pausas" na aplicação. Para minimizar o impacto:
- Evite criar muitos objetos temporários em loops críticos.
- Use pools de objetos para reutilização em vez de criar novos.
- Desative o GC manualmente apenas em casos extremos (não recomendado para produção).
Exemplo de pool de objetos para buffers:
class BufferPool { constructor(size) { this.pool = []; for (let i = 0; i