Node.js em produção: performance e PM2 cluster
Node.js roda em uma única thread por padrão — em uma VPS com 4 CPUs, você aproveita apenas 25% do processador. PM2 em modo cluster e Worker Threads mudam isso, distribuindo a carga entre todos os núcleos. Combinado com compressão, cache de respostas e profiling, você multiplica o throughput sem mudar o hardware.
PM2 em modo cluster
Aproveitar todos os CPUs da VPS com PM2:
# ecosystem.config.js
module.exports = {
apps: [{
name: 'minha-api',
script: './dist/index.js',
instances: 'max', // um processo por CPU
exec_mode: 'cluster', // compartilhar porta entre processos
watch: false,
max_memory_restart: '500M',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
// Zero-downtime deploy:
wait_ready: true,
listen_timeout: 10000,
kill_timeout: 5000,
}],
}
# Iniciar em produção:
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup # gerar comando para iniciar no boot
# Reload sem downtime (reinicia instâncias uma a uma):
pm2 reload minha-api
# Monitorar em tempo real:
pm2 monitWorker Threads para tarefas CPU-intensivas
Mover processamento pesado para threads separadas:
// worker.ts — tarefa CPU-intensiva
import { workerData, parentPort } from 'worker_threads'
function processarRelatorio(dados: unknown[]) {
// processamento pesado (PDF, imagem, cálculos)
return dados.length * 2
}
parentPort!.postMessage(processarRelatorio(workerData))
// api.ts — usar o worker
import { Worker } from 'worker_threads'
import path from 'path'
function executarWorker(dados: unknown[]): Promise<number> {
return new Promise((resolve, reject) => {
const worker = new Worker(
path.resolve(__dirname, 'worker.js'),
{ workerData: dados }
)
worker.on('message', resolve)
worker.on('error', reject)
})
}
app.post('/relatorio', async (req, res) => {
// Não bloqueia o event loop principal:
const resultado = await executarWorker(req.body.dados)
res.json({ resultado })
})Compressão e cache de respostas
Reduzir bytes trafegados e evitar reprocessamento:
import express from 'express'
import compression from 'compression'
import NodeCache from 'node-cache'
const app = express()
const cache = new NodeCache({ stdTTL: 60 }) // cache de 60s
// Compressão gzip/brotli (reduz payload 60-80%):
app.use(compression({
threshold: 1024, // comprimir apenas respostas > 1KB
level: 6, // nível de compressão (1-9, 6 é o balanço ideal)
}))
// Middleware de cache em memória para rotas pesadas:
function cacheMiddleware(ttl: number) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const chave = req.originalUrl
const cached = cache.get(chave)
if (cached) {
return res.json(cached)
}
const originalJson = res.json.bind(res)
res.json = (body) => {
cache.set(chave, body, ttl)
return originalJson(body)
}
next()
}
}
// Aplicar cache em rota pesada:
app.get('/api/relatorio-mensal', cacheMiddleware(300), async (req, res) => {
const dados = await gerarRelatorio() // executa apenas se não estiver no cache
res.json(dados)
})Profiling com Node.js --inspect e Clinic.js
Identificar gargalos de performance na aplicação:
# Instalar Clinic.js (suite de profiling para Node.js):
npm install -g clinic
# Doctor: diagnóstico geral (I/O, event loop lag, memory):
clinic doctor -- node dist/index.js
# Acessar: http://localhost:3000 com carga e pressionar Ctrl+C
# Flame: flamegraph de CPU (onde o tempo está sendo gasto):
clinic flame -- node dist/index.js
# Bubbleprof: análise do event loop e async/await:
clinic bubbleprof -- node dist/index.js
# Gerar carga durante o profiling:
# Em outro terminal:
npx autocannon -c 100 -d 30 http://localhost:3000/api/endpoint
# Profiling nativo do Node.js (sem ferramentas externas):
node --prof dist/index.js &
# Gerar carga...
# Analisar:
node --prof-process isolate-*.log > processed.txt
cat processed.txt | head -100Variáveis de ambiente e configuração de produção
Configurações do Node.js que impactam performance:
# Variáveis de ambiente importantes para produção:
# Habilitar otimizações do V8:
NODE_ENV=production
# Aumentar memória disponível (padrão é ~1.5GB):
NODE_OPTIONS=--max-old-space-size=4096
# UV_THREADPOOL_SIZE: threads para I/O assíncrono (padrão: 4)
# Aumentar para workloads com muito I/O simultâneo:
UV_THREADPOOL_SIZE=16
# ecosystem.config.js com variáveis:
module.exports = {
apps: [{
name: 'api',
script: './dist/index.js',
instances: 'max',
exec_mode: 'cluster',
node_args: '--max-old-space-size=512', // por instância
env_production: {
NODE_ENV: 'production',
UV_THREADPOOL_SIZE: 16,
},
}],
}
# Verificar event loop lag em produção:
# Adicionar ao código:
import { monitorEventLoopDelay } from 'perf_hooks'
const h = monitorEventLoopDelay({ resolution: 20 })
h.enable()
setInterval(() => {
console.log('Event loop lag (p99):', h.percentile(99) / 1e6, 'ms')
}, 30_000)$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para Node.js em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.
Perguntas frequentes
Conteúdos relacionados