跳轉到內容

Node.js 性能調優與內存洩漏排查實戰 2026

Node.js 性能調優

💡 性能調優的重要性:Node.js 以其高性能異步 I/O 著稱,但在生產環境中,不當的代碼編寫或配置可能導致嚴重的性能問題。本文將帶你掌握完整的性能調優方法論。

本文將帶你從 0 到 1 掌握:

  • ✅ Node.js 性能監控工具鏈
  • ✅ 內存洩漏檢測與排查
  • ✅ CPU 性能分析與優化
  • ✅ 異步編程優化策略
  • ✅ 性能測試與基準測試
  • ✅ 生產環境性能監控方案

一、性能監控基礎

1.1 Node.js 內置監控

javascript
// process 對象提供的性能指標
const os = require('os');

// CPU 使用率
const cpuUsage = process.cpuUsage();
console.log('CPU 使用時間:', cpuUsage);

// 內存使用
const memUsage = process.memoryUsage();
console.log('內存使用:', {
  rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`,      // 駐留集大小
  heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,  // 堆總大小
  heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,    // 堆使用量
  external: `${(memUsage.external / 1024 / 1024).toFixed(2)} MB`     // 外部內存
});

// 事件循環延遲
const start = process.hrtime();
setImmediate(() => {
  const diff = process.hrtime(start);
  console.log('事件循環延遲:', `${diff[0] * 1000 + diff[1] / 1e6}ms`);
});

1.2 常用監控工具

工具用途安裝方式
clinic.js全面性能分析套件npm install -g clinic
node-inspectChrome DevTools 調試內置
pm2進程管理與監控npm install -g pm2
newrelicAPM 監控服務npm install newrelic
prom-clientPrometheus 指標採集npm install prom-client

1.3 clinic.js 使用指南

bash
# 安裝 clinic
npm install -g clinic

# CPU 分析
clinic flame --on-port 'autocannon http://localhost:3000' -- node app.js

# 內存分析
clinic heap-profiler --on-port 'autocannon http://localhost:3000' -- node app.js

# 阻塞分析
clinic bubbleprof --on-port 'autocannon http://localhost:3000' -- node app.js

# 事件循環分析
clinic eventloop --on-port 'autocannon http://localhost:3000' -- node app.js

二、內存洩漏排查

2.1 內存洩漏類型

類型描述常見原因
全局變量洩漏未聲明的變量自動掛載到 global缺少 var/let/const
閉包洩漏閉包引用了外部變量定時器、事件監聽器
緩存洩漏緩存對象無限增長未設置過期策略
DOM 洩漏在 Node.js 中較少見瀏覽器環境
EventEmitter 洩漏事件監聽器未移除未調用 removeListener

2.2 內存洩漏檢測步驟

javascript
// 1. 啟用堆快照
// node --inspect=0.0.0.0:9229 app.js

// 2. 使用 Chrome DevTools 捕獲堆快照

// 3. 分析對比快照

// 4. 定位洩漏源

2.3 常見內存洩漏模式

模式 1:未清理的定時器

javascript
// ❌ 錯誤:定時器永遠不會被清除
setInterval(() => {
  // 一些操作
}, 1000);

// ✅ 正確:保存引用並在適當時候清除
const intervalId = setInterval(() => {
  // 一些操作
}, 1000);

// 不再需要時清除
clearInterval(intervalId);

模式 2:事件監聽器洩漏

javascript
// ❌ 錯誤:監聽器累積
const emitter = new EventEmitter();

function handler() {
  console.log('event');
}

// 每次調用都會添加新的監聽器
emitter.on('event', handler);

// ✅ 正確:使用 once 或手動移除
emitter.once('event', handler);  // 只觸發一次

// 或
emitter.on('event', handler);
emitter.removeListener('event', handler);

模式 3:緩存無限增長

javascript
// ❌ 錯誤:緩存無限增長
const cache = {};

function getData(key) {
  if (!cache[key]) {
    cache[key] = expensiveComputation(key);
  }
  return cache[key];
}

// ✅ 正確:設置緩存上限
class LimitedCache {
  constructor(maxSize = 1000) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  set(key, value) {
    if (this.cache.size >= this.maxSize) {
      // 刪除最老的條目
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, value);
  }

  get(key) {
    return this.cache.get(key);
  }
}

模式 4:閉包引用洩漏

javascript
// ❌ 錯誤:閉包保留了大對象引用
function createBigObject() {
  const bigData = Array(1000000).fill('x');
  
  return function() {
    // 即使不使用 bigData,它也會被閉包保留
    console.log('hello');
  };
}

const fn = createBigObject();
// bigData 仍然在內存中

// ✅ 正確:釋放不需要的引用
function createCleanFunction() {
  let bigData = Array(1000000).fill('x');
  
  const result = function() {
    console.log('hello');
  };
  
  bigData = null;  // 釋放引用
  return result;
}

2.4 使用 heapdump 分析內存

javascript
const heapdump = require('heapdump');

// 在特定條件下觸發快照
if (memoryUsage > threshold) {
  const snapshotPath = `heap-${Date.now()}.heapsnapshot`;
  heapdump.writeSnapshot(snapshotPath);
  console.log(`Heap snapshot written to ${snapshotPath}`);
}

// 或通過信號觸發
// kill -USR2 <pid>

2.5 使用 Chrome DevTools 分析

bash
# 啟動調試模式
node --inspect=0.0.0.0:9229 app.js

# 訪問 chrome://inspect
# 選擇你的 Node.js 進程
# 打開 Memory 面板
# 點擊 Take snapshot

分析步驟:

  1. 捕獲初始快照(Snapshot 1)
  2. 執行可能導致洩漏的操作
  3. 捕獲第二個快照(Snapshot 2)
  4. 切換到 Comparison 視圖
  5. 查找增長異常的對象類型
  6. 分析保留路徑(Retainers)

三、CPU 性能優化

3.1 CPU 密集型任務處理

javascript
// ❌ 錯誤:阻塞事件循環
function heavyComputation() {
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }
  return result;
}

// ✅ 正確:使用 setImmediate 分塊處理
function chunkedComputation(total, chunkSize, callback) {
  let processed = 0;
  let result = 0;

  function processChunk() {
    const end = Math.min(processed + chunkSize, total);
    for (let i = processed; i < end; i++) {
      result += i;
    }
    processed = end;

    if (processed < total) {
      setImmediate(processChunk);
    } else {
      callback(null, result);
    }
  }

  setImmediate(processChunk);
}

3.2 使用 Worker Threads

javascript
// worker.js
const { parentPort } = require('worker_threads');

parentPort.on('message', (data) => {
  const result = heavyComputation(data);
  parentPort.postMessage(result);
});

// main.js
const { Worker } = require('worker_threads');

function runWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js');
    worker.postMessage(data);
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) {
        reject(new Error(`Worker exited with code ${code}`));
      }
    });
  });
}

3.3 使用集群模式

javascript
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

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

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

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();  // 重啟進程
  });
} else {
  // 工作進程運行應用
  require('./app.js');
  console.log(`Worker ${process.pid} started`);
}

3.4 使用 PM2 管理進程

bash
# 安裝 PM2
npm install -g pm2

# 啟動應用(自動使用集群模式)
pm2 start app.js -i max

# 查看狀態
pm2 status

# 查看日誌
pm2 logs

# 監控面板
pm2 monit

# 停止應用
pm2 stop app

# 重啟應用
pm2 restart app

四、異步編程優化

4.1 Promise 優化

javascript
// ❌ 錯誤:串行執行,效率低
async function fetchData() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts.map(p => p.id));
  return { user, posts, comments };
}

// ✅ 正確:並行執行
async function fetchDataOptimized() {
  const user = await fetchUser();
  const postsPromise = fetchPosts(user.id);
  // 可以並行執行的操作
  const [posts, profile] = await Promise.all([
    postsPromise,
    fetchProfile(user.id)
  ]);
  const comments = await fetchComments(posts.map(p => p.id));
  return { user, posts, comments, profile };
}

4.2 Stream 處理大數據

javascript
// ❌ 錯誤:一次性加載到內存
const fs = require('fs');
const data = fs.readFileSync('large-file.csv', 'utf8');
const lines = data.split('\n');
processLines(lines);

// ✅ 正確:使用 Stream
const { createReadStream } = require('fs');
const { createInterface } = require('readline');

const rl = createInterface({
  input: createReadStream('large-file.csv'),
  crlfDelay: Infinity
});

rl.on('line', (line) => {
  processLine(line);
});

rl.on('close', () => {
  console.log('處理完成');
});

4.3 使用高效的數據結構

javascript
// ❌ 錯誤:使用數組進行頻繁查找
const users = [];

function findUserById(id) {
  return users.find(u => u.id === id);  // O(n)
}

// ✅ 正確:使用 Map 進行 O(1) 查找
const usersMap = new Map();

function addUser(user) {
  usersMap.set(user.id, user);
}

function findUserByIdOptimized(id) {
  return usersMap.get(id);  // O(1)
}

五、性能測試與基準測試

5.1 使用 autocannon 進行負載測試

bash
# 安裝 autocannon
npm install -g autocannon

# 基本測試
autocannon http://localhost:3000/api/users

# 高級測試
autocannon \
  -c 100 \          # 併發連接數
  -d 30 \           # 持續時間(秒)
  -p 10 \           # 管道數
  -H "Authorization: Bearer token" \
  http://localhost:3000/api/users

# 輸出詳細報告
autocannon -c 50 -d 10 http://localhost:3000 --json | jq .

5.2 使用 benchmark.js 進行基準測試

javascript
const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;

suite
  .add('RegExp#test', function() {
    /o/.test('Hello World!');
  })
  .add('String#indexOf', function() {
    'Hello World!'.indexOf('o') > -1;
  })
  .on('cycle', function(event) {
    console.log(String(event.target));
  })
  .on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ 'async': true });

5.3 使用 Artillery 進行場景測試

yaml
# artillery.yaml
config:
  target: 'http://localhost:3000'
  phases:
    - duration: 60
      arrivalRate: 10
      rampTo: 100
    - duration: 120
      arrivalRate: 100

scenarios:
  - name: 'API 測試'
    flow:
      - get:
          url: '/api/users'
      - post:
          url: '/api/login'
          json:
            username: 'test'
            password: 'test'
bash
# 運行測試
artillery run artillery.yaml

# 生成報告
artillery run artillery.yaml --output report.json
artillery report report.json

六、V8 引擎優化

6.1 內存分配優化

javascript
// ❌ 錯誤:頻繁創建臨時對象
function processItems(items) {
  return items.map(item => {
    return {
      id: item.id,
      name: item.name,
      processed: true
    };
  });
}

// ✅ 正確:複用對象(如果可能)
const reusableObj = { id: null, name: null, processed: false };

function processItemsOptimized(items) {
  const results = [];
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    results.push({
      id: item.id,
      name: item.name,
      processed: true
    });
  }
  return results;
}

6.2 避免隱式類型轉換

javascript
// ❌ 錯誤:隱式類型轉換
function compare(a, b) {
  return a == b;  // 可能觸發類型轉換
}

// ✅ 正確:嚴格相等
function compareOptimized(a, b) {
  return a === b;
}

// ❌ 錯誤:字符串拼接中的隱式轉換
let result = '';
for (let i = 0; i < 1000; i++) {
  result += i;  // 每次都創建新字符串
}

// ✅ 正確:使用數組
const parts = [];
for (let i = 0; i < 1000; i++) {
  parts.push(i);
}
const result = parts.join('');

6.3 使用正確的迭代方式

javascript
const arr = Array(1000000).fill(0);

// ❌ 慢:forEach
arr.forEach((item, index) => {
  // 操作
});

// ✅ 快:for 循環
for (let i = 0, len = arr.length; i < len; i++) {
  const item = arr[i];
  // 操作
}

// ✅ 更快:while 循環
let i = arr.length;
while (i--) {
  const item = arr[i];
  // 操作
}

七、生產環境監控方案

7.1 使用 Prometheus + Grafana

javascript
const client = require('prom-client');

// 定義指標
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP 請求持續時間',
  labelNames: ['method', 'route', 'status_code']
});

const memoryUsage = new client.Gauge({
  name: 'nodejs_memory_usage_bytes',
  help: 'Node.js 內存使用量',
  labelNames: ['type']
});

// 中間件:記錄請求時間
function prometheusMiddleware(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration.labels(req.method, req.path, res.statusCode).observe(duration);
  });
  next();
}

// 定期記錄內存使用
setInterval(() => {
  const mem = process.memoryUsage();
  memoryUsage.labels('rss').set(mem.rss);
  memoryUsage.labels('heapTotal').set(mem.heapTotal);
  memoryUsage.labels('heapUsed').set(mem.heapUsed);
}, 10000);

// 暴露指標端點
app.get('/metrics', (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.send(client.register.metrics());
});

7.2 PM2 監控配置

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-app',
    script: 'app.js',
    instances: 'max',
    exec_mode: 'cluster',
    watch: true,
    max_memory_restart: '500M',  // 內存超過 500MB 自動重啟
    env: {
      NODE_ENV: 'production'
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss'
  }],

  deploy: {
    production: {
      user: 'deploy',
      host: ['server1', 'server2'],
      ref: 'origin/main',
      repo: 'git@github.com:user/repo.git',
      path: '/var/www/production',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
    }
  }
};

7.3 設置告警規則

yaml
# Prometheus alert rules
groups:
- name: nodejs_alerts
  rules:
  - alert: HighMemoryUsage
    expr: nodejs_memory_usage_bytes{type="heapUsed"} / nodejs_memory_usage_bytes{type="heapTotal"} > 0.8
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "高內存使用率"
      description: "內存使用率超過 80% (當前: {{ $value }}%)"

  - alert: HighCPUUsage
    expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[1m])) * 100) > 80
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "高 CPU 使用率"
      description: "CPU 使用率超過 80% (當前: {{ $value }}%)"

  - alert: HighRequestLatency
    expr: avg(http_request_duration_seconds) > 1
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "高請求延遲"
      description: "平均請求延遲超過 1 秒"

八、常見性能問題與解決方案

Q1:事件循環阻塞

javascript
// 問題:同步操作阻塞事件循環
function processData(data) {
  // 耗時的同步操作
  return data.map(item => expensiveTransformation(item));
}

// 解決方案:使用 setImmediate 分塊處理
function processDataAsync(data, chunkSize = 100) {
  let index = 0;
  
  return new Promise((resolve) => {
    function processChunk() {
      const end = Math.min(index + chunkSize, data.length);
      for (let i = index; i < end; i++) {
        data[i] = expensiveTransformation(data[i]);
      }
      index = end;
      
      if (index < data.length) {
        setImmediate(processChunk);
      } else {
        resolve(data);
      }
    }
    
    setImmediate(processChunk);
  });
}

Q2:內存持續增長

javascript
// 問題:未清理的定時器或監聽器
class DataProcessor {
  constructor() {
    this.interval = setInterval(() => {
      this.fetchData();
    }, 1000);
  }
  
  destroy() {
    // 忘記清除定時器
  }
}

// 解決方案:確保清理資源
class DataProcessorFixed {
  constructor() {
    this.interval = setInterval(() => {
      this.fetchData();
    }, 1000);
  }
  
  destroy() {
    clearInterval(this.interval);
    this.interval = null;
  }
}

Q3:數據庫查詢慢

javascript
// 問題:N+1 查詢問題
async function getUsersWithPosts() {
  const users = await User.findAll();
  return Promise.all(users.map(async user => {
    const posts = await Post.findAll({ where: { userId: user.id } });
    return { ...user, posts };
  }));
}

// 解決方案:使用預加載
async function getUsersWithPostsOptimized() {
  return await User.findAll({
    include: [{ model: Post }]
  });
}

Q4:大量小文件讀取

javascript
// 問題:串行讀取多個文件
async function loadFiles(filePaths) {
  const contents = [];
  for (const path of filePaths) {
    const content = await fs.promises.readFile(path, 'utf8');
    contents.push(content);
  }
  return contents;
}

// 解決方案:並行讀取
async function loadFilesOptimized(filePaths) {
  return await Promise.all(
    filePaths.map(path => fs.promises.readFile(path, 'utf8'))
  );
}

九、性能調優檢查表

✅ 代碼層面

✅ 異步編程

✅ 內存管理

✅ 部署層面

✅ 監控層面


結語

Node.js 性能調優是一個系統性工程,需要從代碼、架構、部署、監控等多個層面入手。通過本文的學習,你已經掌握了:

  1. 性能監控工具:clinic.js、Chrome DevTools、PM2
  2. 內存洩漏排查:堆快照分析、常見洩漏模式識別
  3. CPU 優化:Worker Threads、集群模式、分塊處理
  4. 異步編程優化:Promise.all、Stream、高效數據結構
  5. 生產環境監控:Prometheus、Grafana、告警配置

推薦閱讀:


🚀 提示: 性能調優是一個持續的過程,建議建立性能基線,定期進行性能測試,及時發現和解決性能瓶頸。


延伸阅读

免责声明

本文仅供技术交流和学习参考。涉及第三方服务的链接可能包含 sponsored 标记,请自行核实服务条款、价格和可用性,并遵守当地法律法规。

最後更新於: