Introdução: Por que o desempenho do JavaScript é importante?
\nJavaScript (especialmente no ambiente Node.js) tornou-se a base do desenvolvimento web moderno. A velocidade de execução do código afeta não apenas a experiência do usuário, mas também a carga no servidor. Aplicações lentas perdem audiência, e requisições não otimizadas podem levar a um aumento caro na capacidade. Neste artigo, abordaremos métodos básicos, mas eficazes, de otimização de desempenho que são adequados até mesmo para iniciantes.
\nVamos considerar três áreas principais: programação assíncrona, gerenciamento de memória e profiling. Todos os exemplos serão em Node.js.
\n\n1. Assincronia: Evitando o bloqueio do Event Loop
\nO Node.js opera em modo single-thread com o Event Loop. Se você executa uma operação síncrona pesada (por exemplo, ler um arquivo grande ou um loop complexo), todo o servidor "congela" até sua conclusão. Para evitar isso, use métodos assíncronos.
\nExemplo ruim (código bloqueante):
\nconst fs = require('fs');\nconst data = fs.readFileSync('/bigfile.txt', 'utf8'); // Bloqueia a thread\nconsole.log('Arquivo lido');\n\nExemplo bom (não bloqueante):
\nconst fs = require('fs').promises;\nasync function readFile() {\n const data = await fs.readFile('/bigfile.txt', 'utf8');\n console.log('Arquivo lido');\n}\nreadFile();\n\nUse async/await e Promises para todas as operações de entrada/saída (I/O). Isso permite que o Event Loop processe outras requisições enquanto aguarda a resposta do banco de dados ou sistema de arquivos.
Dica: Evite métodos síncronos em produção
\nMétodos como fs.readFileSync ou crypto.pbkdf2Sync são aceitáveis apenas na inicialização da aplicação. Em tempo de execução, use suas versões assíncronas.
2. Otimização do gerenciamento de memória e vazamentos
\nVazamentos de memória são uma das causas comuns de queda de desempenho em aplicações Node.js. Eles ocorrem quando objetos permanecem na memória, mesmo não sendo mais necessários.
\nPrincipais causas de vazamentos:
\n- \n
- Variáveis globais \n
- Timers não limpos (setInterval sem clearInterval) \n
- Referências fechadas em closures \n
- Listeners de eventos que não são removidos \n
Exemplo de vazamento através de listeners:
\nconst EventEmitter = require('events');\nconst emitter = new EventEmitter();\n\nfunction processRequest() {\n emitter.on('data', (chunk) => {\n // Processamento de dados\n });\n}\n// Cada chamada a processRequest adiciona um novo listener, mas não remove o antigo!\n\nSolução: Remova listeners ou use once
\nemitter.once('data', (chunk) => {\n // Será executado apenas uma vez e removido automaticamente\n});\n\nTambém é útil usar utilitários como heapdump para análise de memória. Mas, para começar, basta garantir que as referências a objetos não permaneçam no escopo global.
3. Cache e Profiling
\nO cache permite evitar a reexecução de cálculos pesados. O profiling ajuda a encontrar gargalos no código.
\nCache simples com Map:
\nconst cache = new Map();\n\nfunction expensiveCalculation(input) {\n if (cache.has(input)) {\n console.log('Do cache');\n return cache.get(input);\n }\n const result = input * 1000 + Math.random(); // Simulação de lógica complexa\n cache.set(input, result);\n return result;\n}\n\nconsole.log(expensiveCalculation(5)); // Calcula\nconsole.log(expensiveCalculation(5)); // Do cache\n\nPara cenários mais complexos, use bibliotecas como node-cache ou Redis.
Profiling com a ferramenta integrada do Node.js
\nExecute a aplicação com a flag --prof:
node --prof app.js\n\nApós parar a aplicação, um arquivo isolate-*.log aparecerá na pasta. Converta-o em