强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 21 章 · 性能优化

第 21 章 · 性能优化

21.1 性能优化概述

性能瓶颈分类

瓶颈类型表现解决方案
I/O 瓶颈数据库慢、网络延迟缓存、连接池、异步
CPU 瓶颈计算密集、事件循环阻塞Worker Threads、Cluster
内存瓶颈内存泄漏、OOM 崩溃内存分析、优化数据结构
网络瓶颈响应慢、带宽不足压缩、CDN、分页

21.2 Cluster 模块

Node.js 单线程只能利用一个 CPU 核心。Cluster 模块可以创建多个工作进程,充分利用多核 CPU。

const cluster = require('cluster');
const http = require('http');
const os = require('os');

const numCPUs = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`主进程 ${process.pid} 正在运行`);
  console.log(`启动 ${numCPUs} 个工作进程`);

  // Fork 工作进程
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // 工作进程退出时重启
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 退出 (${signal || code}),重启中...`);
    cluster.fork();
  });

  // 优雅重启
  process.on('SIGUSR2', () => {
    const workers = Object.values(cluster.workers);
    function restartWorker(i) {
      if (i >= workers.length) return;
      const worker = workers[i];
      console.log(`重启工作进程 ${worker.process.pid}`);
      worker.disconnect();
      worker.on('disconnect', () => {
        const newWorker = cluster.fork();
        newWorker.on('listening', () => restartWorker(i + 1));
      });
    }
    restartWorker(0);
  });
} else {
  const app = require('./app');
  const server = app.listen(3000, () => {
    console.log(`工作进程 ${process.pid} 监听端口 3000`);
  });
}

使用 pm2 管理 Cluster(推荐)

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-app',
    script: 'src/server.js',
    instances: 'max',      // 使用所有 CPU 核心
    exec_mode: 'cluster',
    max_memory_restart: '1G',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
  }],
};
# 启动
pm2 start ecosystem.config.js --env production

# 管理
pm2 status
pm2 logs
pm2 reload all    # 零停机重启
pm2 stop all
pm2 delete all

21.3 Worker Threads

适用于 CPU 密集型任务,不会阻塞主事件循环。

// worker-pool.js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // 主线程
  function runTask(workerData) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, { workerData });
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
      });
    });
  }

  // 并行执行 CPU 密集型任务
  async function main() {
    const start = Date.now();
    const results = await Promise.all([
      runTask({ start: 0, end: 1e7 }),
      runTask({ start: 1e7, end: 2e7 }),
      runTask({ start: 2e7, end: 3e7 }),
      runTask({ start: 3e7, end: 4e7 }),
    ]);
    const total = results.reduce((a, b) => a + b, 0);
    console.log(`结果: ${total}, 耗时: ${Date.now() - start}ms`);
  }

  main();
} else {
  // 工作线程
  function heavyComputation(start, end) {
    let sum = 0;
    for (let i = start; i < end; i++) {
      sum += Math.sqrt(i);
    }
    return sum;
  }

  const result = heavyComputation(workerData.start, workerData.end);
  parentPort.postMessage(result);
}

Worker Threads 线程池

const { Worker } = require('worker_threads');
const os = require('os');

class ThreadPool {
  constructor(workerFile, poolSize = os.cpus().length) {
    this.workerFile = workerFile;
    this.pool = [];
    this.queue = [];

    for (let i = 0; i < poolSize; i++) {
      this.pool.push({ worker: new Worker(workerFile), busy: false });
    }
  }

  run(task) {
    return new Promise((resolve, reject) => {
      const available = this.pool.find(w => !w.busy);
      if (available) {
        this._execute(available, task, resolve, reject);
      } else {
        this.queue.push({ task, resolve, reject });
      }
    });
  }

  _execute(wrapper, task, resolve, reject) {
    wrapper.busy = true;
    const worker = wrapper.worker;

    worker.postMessage(task);
    worker.once('message', (result) => {
      resolve(result);
      wrapper.busy = false;
      if (this.queue.length > 0) {
        const { task, resolve, reject } = this.queue.shift();
        this._execute(wrapper, task, resolve, reject);
      }
    });
    worker.once('error', reject);
  }

  async shutdown() {
    await Promise.all(this.pool.map(w => w.worker.terminate()));
  }
}

21.4 内存分析

监控内存使用

// 内存使用监控
function logMemoryUsage() {
  const mem = process.memoryUsage();
  console.log({
    rss: `${Math.round(mem.rss / 1024 / 1024)}MB`,        // 进程总内存
    heapTotal: `${Math.round(mem.heapTotal / 1024 / 1024)}MB`, // 堆总量
    heapUsed: `${Math.round(mem.heapUsed / 1024 / 1024)}MB`,  // 堆已用
    external: `${Math.round(mem.external / 1024 / 1024)}MB`,   // C++ 对象
    arrayBuffers: `${Math.round(mem.arrayBuffers / 1024 / 1024)}MB`,
  });
}

setInterval(logMemoryUsage, 30000); // 每 30 秒记录

生成 Heap Snapshot

const v8 = require('v8');
const fs = require('fs');

// 手动生成堆快照
function takeHeapSnapshot() {
  const snapshotStream = v8.writeHeapSnapshot();
  console.log(`堆快照已保存: ${snapshotStream}`);
  // 用 Chrome DevTools 打开 .heapsnapshot 文件分析
}

// 监控内存,超过阈值时自动采集
const MAX_HEAP = 500 * 1024 * 1024; // 500MB
setInterval(() => {
  if (process.memoryUsage().heapUsed > MAX_HEAP) {
    takeHeapSnapshot();
  }
}, 10000);
# 使用 --inspect 启动后,Chrome DevTools Memory 面板可以:
# 1. 拍摄堆快照(Heap Snapshot)
# 2. 记录内存分配时间线(Allocation Timeline)
# 3. 记录内存分配采样(Allocation Sampling)

常见内存泄漏

// ❌ 泄漏 1:无限制的缓存
const cache = {};
function addToCache(key, value) {
  cache[key] = value; // 永远不会被清理
}

// ✅ 使用 LRU 缓存
const { LRUCache } = require('lru-cache');
const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 });

// ❌ 泄漏 2:未移除的事件监听器
class LeakyEmitter extends EventEmitter {
  constructor() {
    super();
    // 每次创建实例都会增加监听器
    setInterval(() => {
      this.emit('data', Date.now());
    }, 1000);
  }
}

// ❌ 泄漏 3:闭包持有大对象
function processData() {
  const largeData = new Array(1000000).fill('x');
  return function getLength() {
    return largeData.length; // largeData 被闭包持有,无法回收
  };
}

// ✅ 只保留需要的值
function processData() {
  const largeData = new Array(1000000).fill('x');
  const length = largeData.length;
  return function getLength() {
    return length;
  };
}

21.5 CPU Profiling

# 方式 1:命令行
node --prof app.js
node --prof-process isolate-*.log > processed.txt

# 方式 2:使用 --inspect + Chrome DevTools
node --inspect app.js
# 打开 chrome://inspect → Profiler → 开始录制

# 方式 3:使用 perf_hooks
node --cpu-prof app.js
// 使用 perf_hooks 进行精确计时
const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(3)}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

function benchmark(name, fn, iterations = 1000) {
  performance.mark(`${name}-start`);
  for (let i = 0; i < iterations; i++) fn();
  performance.mark(`${name}-end`);
  performance.measure(name, `${name}-start`, `${name}-end`);
}

benchmark('JSON.parse', () => JSON.parse('{"a":1}'));
benchmark('正则匹配', () => /test/.test('this is a test'));

21.6 通用优化技巧

数据库优化

// ✅ 使用连接池
const pool = mysql.createPool({ connectionLimit: 20 });

// ✅ 批量查询代替循环查询
// ❌ N+1 问题
for (const order of orders) {
  order.user = await db.users.findById(order.userId);
}
// ✅ 一次查询
const userIds = orders.map(o => o.userId);
const users = await db.users.find({ id: { $in: userIds } });
const userMap = new Map(users.map(u => [u.id, u]));
orders.forEach(o => o.user = userMap.get(o.userId));

// ✅ 只查询需要的字段
const users = await prisma.user.findMany({
  select: { id: true, name: true }, // 不要 select *
});

// ✅ 添加索引
// 频繁 WHERE 的字段添加索引

缓存策略

const { createClient } = require('redis');
const redis = createClient();

async function getCachedUser(id) {
  const cacheKey = `user:${id}`;
  
  // 先查缓存
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // 缓存未命中,查数据库
  const user = await db.users.findById(id);
  if (user) {
    await redis.setEx(cacheKey, 300, JSON.stringify(user)); // 缓存 5 分钟
  }
  return user;
}

注意事项

⚠️ 先测量再优化:不要盲目优化,使用 Profiler 找到真正的瓶颈。

⚠️ Cluster 不适合 CPU 密集型任务:Cluster 适合 I/O 密集型 Web 服务,CPU 密集型应使用 Worker Threads。

⚠️ 注意内存泄漏:定期检查内存使用,使用 --max-old-space-size 限制堆大小。

业务场景

  1. 高并发 API:Cluster 模式充分利用多核 CPU
  2. 数据处理:Worker Threads 并行处理大量数据
  3. 图片处理:使用 Worker Threads 避免阻塞事件循环
  4. 缓存层:Redis 缓存热点数据,减少数据库压力

扩展阅读


上一章第 20 章 · 安全 下一章第 22 章 · Docker 部署 — Dockerfile、多阶段构建和 PM2。