DevToolBoxฟรี
บล็อก

Node.js Performance Optimization: Clustering, Streams, Profiling

12 นาทีโดย DevToolBox

Node.js สามารถจัดการการเชื่อมต่อพร้อมกันหลายหมื่นรายการบนเซิร์ฟเวอร์เดียว คู่มือนี้ครอบคลุม clustering, streams, profiling และ กลยุทธ์การ caching

ทำความเข้าใจ Event Loop

Node.js เป็น single-threaded การบล็อก event loop จะบล็อกคำขอทั้งหมด

// 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 สำหรับประสิทธิภาพ Multi-Core

Node.js ทำงานบน CPU core เดียวโดยค่าเริ่มต้น

// 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 สำหรับประสิทธิภาพหน่วยความจำ

Streams ช่วยให้ประมวลผลข้อมูลทีละส่วนโดยไม่ต้องโหลดทั้งหมดเข้าหน่วยความจำ

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

กลยุทธ์การ Caching

Caching เป็นการปรับแต่งประสิทธิภาพที่มีผลกระทบสูงที่สุด

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

คำถามที่พบบ่อย

ควรสร้าง worker threads หรือ cluster workers กี่ตัว?

สำหรับ cluster: สร้าง worker หนึ่งตัวต่อ CPU core, os.cpus().length workers

เมื่อไหร่ควรใช้ streams vs โหลดข้อมูลเข้าหน่วยความจำ?

ใช้ streams สำหรับไฟล์ขนาดเกิน 10MB การ pipe ข้อมูล และการประมวลผลแบบ incremental

flag --inspect คืออะไร?

flag --inspect เริ่มต้น Node.js โดยเปิดใช้งาน V8 inspector protocol

ทำไมแอป Node.js ของฉันถึงใช้หน่วยความจำมากขนาดนี้?

สาเหตุทั่วไป: memory leaks, cache โดยไม่มี eviction policy, dataset ขนาดใหญ่ใน memory

เครื่องมือที่เกี่ยวข้อง

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

{ }JSON Formatter

บทความที่เกี่ยวข้อง

Node.js Streams คู่มือครบถ้วน: Readable, Writable, Transform และ Pipeline

เชี่ยวชาญ Node.js streams — readable, writable, transform, pipeline API

Redis Caching Patterns: คู่มือฉบับสมบูรณ์ (2026)

เรียนรู้ pattern การ caching ของ Redis

Docker Best Practices: 20 เคล็ดลับสำหรับ Container ใน Production

เชี่ยวชาญ Docker ด้วย 20 แนวปฏิบัติที่ดี: multi-stage builds, ความปลอดภัย, การเพิ่มประสิทธิภาพ image และ CI/CD