DevToolBoxGRATIS
Blog

Ottimizzazione delle Prestazioni Node.js: Clustering, Stream, Profiling

12 mindi DevToolBox

Node.js può gestire decine di migliaia di connessioni simultanee su un singolo server. Questa guida copre clustering, stream, profiling e strategie di caching.

Capire il ciclo degli eventi

Node.js è single-thread. Bloccare il ciclo degli eventi blocca tutte le richieste.

// 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 per prestazioni multi-core

Node.js viene eseguito su un singolo core CPU per impostazione predefinita.

// 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

Stream per efficienza della memoria

Gli stream permettono di elaborare i dati pezzo per pezzo senza caricare tutto in memoria.

// 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');
}

Strategie di caching

Il caching è l'ottimizzazione delle prestazioni con il maggiore impatto.

// 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);
});

Domande frequenti

Quanti worker del cluster creare?

Crea un worker per core CPU, os.cpus().length worker.

Quando usare gli stream vs caricare in memoria?

Usa gli stream per file superiori a 10 MB, piping di dati e elaborazione incrementale.

Cos'è il flag --inspect?

Il flag --inspect avvia Node.js con il protocollo V8 inspector abilitato.

Perché la mia app Node.js usa così tanta memoria?

Cause comuni: perdite di memoria, cache senza eviction, grandi set di dati in memoria.

Strumenti correlati

𝕏 Twitterin LinkedIn
È stato utile?

Resta aggiornato

Ricevi consigli dev e nuovi strumenti ogni settimana.

Niente spam. Cancella quando vuoi.

Prova questi strumenti correlati

{ }JSON Formatter

Articoli correlati

Node.js Streams: Guida completa Readable, Writable, Transform e Pipeline

Padroneggia gli stream Node.js — readable, writable, transform, pipeline API e backpressure.

Pattern di caching Redis: Guida completa (2026)

Impara i pattern di caching Redis.

Docker Best Practices: 20 consigli per container in produzione

Padroneggia Docker con 20 best practice essenziali: build multi-stage, sicurezza, ottimizzazione immagini e CI/CD.