Introdução: por que o desempenho do JavaScript é importante
JavaScript é a linguagem que alimenta a web moderna. Do frontend ao backend de servidor (Node.js), seu desempenho impacta diretamente a experiência do usuário, a velocidade de carregamento das páginas e os custos de infraestrutura. Na era de aplicações de alta carga e microsserviços, mesmo um pequeno atraso pode custar milhares de usuários a um negócio. Neste artigo, analisaremos métodos práticos de otimização de aplicações JavaScript no Node.js que ajudarão a acelerar seu código, reduzir o consumo de memória e melhorar a capacidade de resposta do sistema.
1. Programação assíncrona e trabalho com Event Loop
Node.js é construído sobre um Event Loop single-thread, e o uso incorreto de assincronicidade é o principal inimigo do desempenho. Operações bloqueantes (como leitura síncrona de arquivos ou cálculos pesados) param o Event Loop, fazendo com que outras requisições esperem.
Use async/await e Promises em vez de callbacks
JavaScript moderno oferece uma sintaxe limpa para trabalhar com assincronicidade. Evite callbacks aninhados (callback hell) — eles complicam o código e podem levar a vazamentos de memória.
// ❌ Ruim: chamada bloqueanteconst data = fs.readFileSync('file.txt', 'utf8');
// ✅ Bom: leitura assíncronaconst fs = require('fs/promises');async function readFile() { const data = await fs.readFile('file.txt', 'utf8'); return data;}Não bloqueie o Event Loop com tarefas pesadas
Se você precisa executar trabalho intensivo de CPU (processamento de imagens, ordenação de grandes arrays), use worker_threads ou child_process. Isso permitirá que a thread principal processe requisições.
// Exemplo com worker_threadsconst { Worker } = require('worker_threads');
function runHeavyTask(data) { return new Promise((resolve, reject) => { const worker = new Worker('./heavy-worker.js', { workerData: data }); worker.on('message', resolve); worker.on('error', reject); });}2. Otimização do gerenciamento de memória e coleta de lixo
Vazamentos de memória no Node.js são um problema comum. Eles levam a uma lentidão gradual da aplicação e a quedas sob carga. Certifique-se de que as referências a objetos não permaneçam em closures ou variáveis globais.
Evite variáveis globais
Objetos globais nunca são removidos pelo garbage collector. Use variáveis locais e módulos.
// ❌ Ruim: array global cresce infinitamenteglobal.cache = [];
// ✅ Bom: use um cache limitado com TTLconst lruCache = new Map();const MAX_SIZE = 100;function addToCache(key, value) { if (lruCache.size >= MAX_SIZE) { const firstKey = lruCache.keys().next().value; lruCache.delete(firstKey); } lruCache.set(key, value);}Use buffers e streams para dados grandes
Ao trabalhar com arquivos ou requisições de rede, não carregue todos os dados na memória de uma vez. Streams processam dados em partes, economizando memória.
// ❌ Ruim: carregamos o arquivo inteiro na memóriaconst data = fs.readFileSync('bigfile.csv', 'utf8');processData(data);
// ✅ Bom: usamos um streamconst readStream = fs.createReadStream('bigfile.csv');readStream.on('data', (chunk) => { processData(chunk);});3. Cache e memoização
Executar repetidamente os mesmos cálculos é um desperdício de recursos. O cache de resultados pode acelerar significativamente a aplicação, especialmente ao trabalhar com bancos de dados ou APIs.
Memoização de funções
Se uma função é chamada com os mesmos argumentos repetidamente, salve o resultado no cache.
function memoize(fn) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const res