Otimização de desempenho de aplicações JavaScript: na prática
O desempenho de aplicações JavaScript não é apenas um bônus agradável, mas um fator criticamente importante que afeta a experiência do usuário (UX), a conversão e os custos de infraestrutura. Um carregamento lento de página ou um atraso na resposta da API pode custar até 40% do seu tráfego. Neste artigo, veremos métodos práticos de otimização de código JavaScript (no ambiente Node.js) que ajudarão você a tornar sua aplicação mais rápida e eficiente. Não vamos nos aprofundar nos fundamentos teóricos do Event Loop, mas sim passar diretamente para técnicas específicas com exemplos.
1. Cache: reduzindo a carga no servidor
Uma das maneiras mais eficazes de acelerar uma aplicação é evitar a repetição de operações caras. O cache permite salvar o resultado de um cálculo ou consulta ao banco de dados e fornecê-lo em uma solicitação repetida.
Cache in-memory usando Map
Para cenários simples, onde os dados não exigem lógica complexa de invalidação, o objeto embutido Map é ideal. Ele é mais rápido que Object e mais adequado para adicionar e remover chaves com frequência.
const cache = new Map();
async function fetchUserData(userId) { if (cache.has(userId)) { console.log('Retornando do cache'); return cache.get(userId); }
// Simulação de uma consulta cara ao BD const data = await new Promise((resolve) => { setTimeout(() => resolve({ id: userId, name: `User_${userId}` }), 1000); });
cache.set(userId, data); return data;}
// Primeira chamada (aguarda 1s)console.time('first');await fetchUserData(1);console.timeEnd('first');
// Segunda chamada (instantânea)console.time('second');await fetchUserData(1);console.timeEnd('second');Cache TTL (Time-To-Live)
Para que o cache não fique desatualizado, é necessário adicionar um tempo de vida ao registro. Vamos implementar um cache TTL simples.
class TTLMap { constructor(ttlMs = 60000) { this.ttl = ttlMs; this.store = new Map(); }
set(key, value) { const record = { value, expiry: Date.now() + this.ttl }; this.store.set(key, record); }
get(key) { const record = this.store.get(key); if (!record) return null; if (Date.now() > record.expiry) { this.store.delete(key); return null; } return record.value; }}
const userCache = new TTLMap(5000); // cache por 5 segundos2. Assincronia e otimização do Event Loop
Node.js é single-threaded, mas sua natureza assíncrona permite processar várias solicitações de forma eficiente. O principal inimigo do desempenho são as operações síncronas que bloqueiam o Event Loop.
Evite versões síncronas de funções
Os módulos fs, crypto e outros possuem equivalentes síncronos (readFileSync, crypto.pbkdf2Sync). Use-os apenas durante a inicialização da aplicação. Dentro dos manipuladores de solicitações, use apenas as versões assíncronas.
// RUIM: Bloqueia o Event Loopconst data = fs.readFileSync('/path/to/file.txt', 'utf-8');
// BOM: Assíncronoconst data = await fs.promises.readFile('/path/to/file.txt', 'utf-8');Divida tarefas pesadas com setImmediate
Se você tem um loop síncrono pesado (por exemplo, processar um array de 10 milhões de elementos), ele bloqueará o servidor por vários segundos. Divida a tarefa em partes e use setImmediate para dar ao Event Loop a chance de processar outras solicitações entre as partes.
function processLargeArray(items, chunkSize = 1000) { let index = 0;
function processChunk() { const chunk = items.slice(index, index + chunkSize); // Processa o pedaço chunk.forEach(item => heavyOperation(item));
index += chunkSize; if (index < items.length) { // Continua na próxima iteração do Event Loop