DevToolBoxFREE
BlogAdvertise

Otimização de Desempenho Node.js: Clustering, Streams, Profiling

12 minby DevToolBox

Node.js pode lidar com dezenas de milhares de conexões simultâneas em um único servidor. Este guia cobre clustering, streams, profiling e estratégias de caching.

Entendendo o Event Loop

Node.js é single-threaded. Bloquear o event loop bloqueia todas as requisições.

// NEVER do this — blocks the event loop
app.get('/compute', (req, res) => {
  // Synchronous CPU-heavy computation blocks ALL requests
  let result = 0;
  for (let i = 0; i < 1e9; i++) result += i;  // 1 billion iterations!
  res.json({ result });
});

// DO THIS instead — offload to worker thread
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

app.get('/compute', (req, res) => {
  const worker = new Worker('./computeWorker.js', {
    workerData: { input: req.query.n }
  });
  worker.on('message', result => res.json({ result }));
  worker.on('error', err => res.status(500).json({ error: err.message }));
});

Clustering para performance multi-core

Node.js roda em um único núcleo de CPU por padrão.

// Node.js Cluster Module — Use All CPU Cores
const cluster = require('cluster');
const os = require('os');
const express = require('express');

const NUM_WORKERS = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  console.log(`Starting ${NUM_WORKERS} workers...`);

  // Fork workers
  for (let i = 0; i < NUM_WORKERS; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
    cluster.fork(); // Auto-restart crashed workers
  });

  cluster.on('online', (worker) => {
    console.log(`Worker ${worker.process.pid} is online`);
  });

} else {
  // Worker process — runs the actual server
  const app = express();

  app.get('/api/users', async (req, res) => {
    const users = await db.getUsers();
    res.json(users);
  });

  app.listen(3000, () => {
    console.log(`Worker ${process.pid} listening on port 3000`);
  });
}

// Alternative: PM2 cluster mode (recommended for production)
// pm2 start server.js -i max   # auto-detect CPU count
// pm2 start server.js -i 4     # explicit count

Streams para eficiência de memória

Streams permitem processar dados pedaço por pedaço sem carregar tudo na memória.

// Node.js Streams — Memory-Efficient Processing

const fs = require('fs');
const { Transform, pipeline } = require('stream');
const { promisify } = require('util');
const pipelineAsync = promisify(pipeline);

// 1. Stream a large file as HTTP response (no memory buffering)
app.get('/download/large-file', (req, res) => {
  const filePath = './large-file.csv';
  const stat = fs.statSync(filePath);

  res.setHeader('Content-Type', 'text/csv');
  res.setHeader('Content-Length', stat.size);
  res.setHeader('Content-Disposition', 'attachment; filename=data.csv');

  // Pipe file directly to response — never fully in memory
  fs.createReadStream(filePath).pipe(res);
});

// 2. Transform stream for CSV processing
class CsvParser extends Transform {
  constructor() {
    super({ objectMode: true });
    this.buffer = '';
    this.headers = null;
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();
    const lines = this.buffer.split('\n');
    this.buffer = lines.pop(); // Keep incomplete line in buffer

    for (const line of lines) {
      if (!this.headers) {
        this.headers = line.split(',');
        continue;
      }
      const values = line.split(',');
      const record = {};
      this.headers.forEach((h, i) => record[h.trim()] = values[i]?.trim());
      this.push(record);
    }
    callback();
  }
}

// 3. Pipeline for reliable error handling
async function processLargeCsvFile(inputPath, outputPath) {
  await pipelineAsync(
    fs.createReadStream(inputPath),
    new CsvParser(),
    new Transform({
      objectMode: true,
      transform(record, enc, cb) {
        // Transform each record
        record.processed = true;
        cb(null, JSON.stringify(record) + '\n');
      }
    }),
    fs.createWriteStream(outputPath)
  );
  console.log('Processing complete');
}

Estratégias de caching

Caching é a otimização de performance de maior impacto.

// Caching Strategies for Node.js

// 1. In-Memory LRU Cache
const { LRUCache } = require('lru-cache');

const cache = new LRUCache({
  max: 500,           // Maximum 500 items
  ttl: 5 * 60 * 1000, // 5 minutes TTL
  allowStale: true,   // Return stale value while refreshing
  updateAgeOnGet: true,
});

async function getUser(id) {
  const cacheKey = `user:${id}`;
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const user = await db.findUser(id);
  cache.set(cacheKey, user);
  return user;
}

// 2. Redis Cache with Stale-While-Revalidate
const Redis = require('ioredis');
const redis = new Redis();

async function getCachedData(key, fetchFn, ttl = 300) {
  const [cached, ttlRemaining] = await redis.pipeline()
    .get(key)
    .ttl(key)
    .exec();

  if (cached[1]) {
    const data = JSON.parse(cached[1]);

    // Background refresh when < 60 seconds remaining
    if (ttlRemaining[1] < 60) {
      fetchFn().then(fresh =>
        redis.setex(key, ttl, JSON.stringify(fresh))
      );
    }

    return data;
  }

  const data = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

// 3. HTTP Response Caching with ETags
app.get('/api/products', async (req, res) => {
  const products = await getProducts();
  const etag = require('crypto')
    .createHash('md5')
    .update(JSON.stringify(products))
    .digest('hex');

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
  res.json(products);
});

Perguntas frequentes

Quantos workers de cluster devo criar?

Crie um worker por núcleo de CPU, os.cpus().length workers.

Quando usar streams vs carregar na memória?

Use streams para arquivos maiores que 10 MB, piping de dados e processamento incremental.

O que é a flag --inspect?

A flag --inspect inicia Node.js com o protocolo V8 inspector habilitado.

Por que meu app Node.js usa tanta memória?

Causas comuns: vazamentos de memória, caches sem eviction, grandes conjuntos de dados na memória.

Ferramentas relacionadas

Isso foi útil?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

Try These Related Tools

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy