Node.js 性能调优指南

面向工程实践的 Node.js 性能优化指南,从测量方法、事件循环、I/O 模型、CPU 密集型任务、内存与 GC 调优,到多进程与部署配置。

以测量为起点,以架构调整为核心,以细节优化为补充。

这份指南面向已经在生产或准生产环境使用 Node.js 的工程团队,希望在 延迟、吞吐量、资源利用 上做系统性优化。

1. 原则:先测量,再优化

在任何优化之前,建议先统一三个基本原则:

  1. 不要凭感觉优化
    • 使用工具确认瓶颈位置:CPU?I/O?数据库?网络?
  2. 优化目标明确
    • 是要降低 p95 延迟?提高 QPS?降低 CPU 占用?
  3. 只优化“热点路径”
    • 优先关注最占用 CPU 时间 / 最多调用的接口 / 最长链路。

1.1 基础压测工具建议

常用压测工具(任选其一):

  • autocannon(Node 社区常用):
  npx autocannon -c 100 -d 30 http://localhost:3000
  • wrk(更底层的 HTTP benchmark)
  • 云平台 / 内部压测平台(如果已有)

建议至少观测:

  • QPS / RPS
  • 平均延迟 / p95 / p99
  • CPU 使用率
  • 内存占用

2. 识别性能瓶颈:Profiling 与观测

2.1 Node 内置 CPU Profiling

简单方式(使用 V8 Profiler):

node --prof app.js
# 运行一段时间后退出
node --prof-process isolate-*.log > processed.txt

然后在 processed.txt 中查看:

  • 哪些函数耗时最高
  • 哪些调用栈在热点路径上

2.2 使用 --inspect + DevTools

Chrome / Edge DevTools 直接连接:

node --inspect app.js
# 或:node --inspect-brk app.js (启动即断点)

可以:

  • 查看 CPU Profile
  • Heap Snapshot
  • 观察 GC 行为

2.3 观测层(推荐)

在生产环境引入 观测系统

  • 请求层指标:QPS、延迟、错误率
  • 运行时指标:CPU、内存、GC 次数
  • Node 自身指标:事件循环延迟(event loop lag)

示例:简单测量事件循环延迟(粗糙版):

const CHECK_INTERVAL = 500
let last = Date.now()

setInterval(() => {
  const now = Date.now()
  const lag = now - last - CHECK_INTERVAL
  last = now
  if (lag > 100) {
    console.warn('Event loop lag:', lag, 'ms')
  }
}, CHECK_INTERVAL)

3. 事件循环与“避免阻塞”

Node 性能的大部分问题,都可以归结为一句话:

不要阻塞事件循环。

3.1 常见“阻塞源”

  1. 同步 I/O 操作

    • fs.readFileSync / fs.writeFileSync
    • fs.readdirSync
  2. 重 CPU 运算

    • 大量循环 / 递归计算
    • 大整数运算 /复杂加密计算
  3. 一次性处理超大 JSON / 数据结构

    • JSON.parse(超大字符串)
    • 创建百万级数组、Map、Set

3.2 同步 I/O → 异步化

不建议:

app.get('/data', (req, res) => {
  const content = fs.readFileSync('big-file.json', 'utf8') // 阻塞
  res.type('json').send(content)
})

建议:

app.get('/data', (req, res) => {
  fs.readFile('big-file.json', 'utf8', (err, content) => {
    if (err) return res.status(500).end()
    res.type('json').send(content)
  })
})

或使用 Promise / async:

import { readFile } from 'node:fs/promises'

app.get('/data', async (req, res) => {
  try {
    const content = await readFile('big-file.json', 'utf8')
    res.type('json').send(content)
  } catch (err) {
    res.status(500).end()
  }
})

3.3 长时间计算 → 拆分任务

如果一定要在主线程做部分计算,可以用 setImmediatesetTimeout(0) 将任务切片,给事件循环“透气”。

function heavyTask(items, chunkSize = 1000) {
  return new Promise((resolve) => {
    let index = 0

    function processChunk() {
      const end = Math.min(index + chunkSize, items.length)
      for (let i = index; i < end; i++) {
        // 处理 items[i]
      }
      index = end
      if (index < items.length) {
        setImmediate(processChunk) // 把控制权交还给事件循环
      } else {
        resolve()
      }
    }

    processChunk()
  })
}

4. I/O 模型与数据库性能

4.1 HTTP 服务实践要点

  • 打开 keep-alive,避免反复建立连接
  • 使用反向代理 / 网关(如 Nginx、Cloudflare、API Gateway)处理 TLS、静态资源
  • 使用适配高并发的框架(如 Fastify)而非过度中间件堆叠

4.2 数据库访问优化

  • 使用 连接池(大多数 ORM / Client 默认支持)

  • 避免 N+1 查询问题

    • 一次请求中循环执行多次 DB 查询
  • 为频繁查询的字段加索引

  • 对长时间查询进行日志记录与追踪

示例:避免 N+1 查询(伪代码对比)

不建议:

const users = await db.query('SELECT * FROM users LIMIT 100')
for (const user of users) {
  user.orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [user.id])
}

建议(一次性批量获取):

const users = await db.query('SELECT * FROM users LIMIT 100')
const ids = users.map(u => u.id)
const orders = await db.query('SELECT * FROM orders WHERE user_id IN (?)', [ids])

// 通过 Map 做 join
const ordersByUserId = new Map()
for (const order of orders) {
  if (!ordersByUserId.has(order.user_id)) {
    ordersByUserId.set(order.user_id, [])
  }
  ordersByUserId.get(order.user_id).push(order)
}

for (const user of users) {
  user.orders = ordersByUserId.get(user.id) || []
}

5. CPU 密集型任务:Worker Threads 与拆分

5.1 何时考虑 Worker Threads?

  • 图像压缩 / 转码
  • 密集加密 / 解密
  • 大量数学运算
  • 大批量 JSON 处理 / 差异计算

5.2 Worker Threads 简单示例

worker.js

const { parentPort, workerData } = require('node:worker_threads')

function heavyComputation(input) {
  // 模拟重计算
  let sum = 0
  for (let i = 0; i < 1e8; i++) {
    sum += i
  }
  return sum + input
}

const result = heavyComputation(workerData.value)
parentPort.postMessage(result)

主线程:

const { Worker } = require('node:worker_threads')

function runHeavyTask(value) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', {
      workerData: { value },
    })

    worker.on('message', resolve)
    worker.on('error', reject)
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with code ${code}`))
    })
  })
}

app.get('/compute', async (req, res) => {
  const value = Number(req.query.value || 1)
  const result = await runHeavyTask(value)
  res.json({ result })
})

通过 Worker:

  • 主线程继续响应其他请求
  • 将重计算移出事件循环

6. 内存与 GC 调优

6.1 常见内存问题

  • 全局变量缓存太多数据
  • 不释放无用的缓存(Map / LRU 未设置上限)
  • EventEmitter 监听器泄漏(未移除)
  • 定时器未清除(setInterval / setTimeout 泄漏)

6.2 Node 内存基本观测

setInterval(() => {
  const m = process.memoryUsage()
  console.log('RSS:', (m.rss / 1024 / 1024).toFixed(1), 'MB',
              'Heap Used:', (m.heapUsed / 1024 / 1024).toFixed(1), 'MB')
}, 10000)

如果 Heap 持续上涨且不回落,可能存在泄漏。

6.3 增加最大堆内存(仅在必要时)

默认堆空间约 1~2GB (与平台有关)。
如果确实需要更大的堆:

node --max-old-space-size=4096 app.js  # 4GB

注意:这是 兜底配置,不能当作“解决内存泄漏”的办法。

7. HTTP 层优化与多核利用

7.1 单进程 vs 多进程

Node 单个进程大多数情况下只使用一个 CPU 核心。

要充分利用多核:

  1. 使用 cluster 模块手动创建 worker
  2. 使用 进程管理工具(推荐) 如 PM2 / 自研守护进程
  3. 使用容器 / K8s 水平扩容多个副本

7.2 简单 cluster 示例(了解即可)

import cluster from 'node:cluster'
import os from 'node:os'
import http from 'node:http'

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length
  console.log(`Master ${process.pid} is running`)
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died, restarting...`)
    cluster.fork()
  })
} else {
  const server = http.createServer((req, res) => {
    res.end(`Handled by ${process.pid}\n`)
  })
  server.listen(3000, () => {
    console.log(`Worker ${process.pid} started`)
  })
}

更推荐交给:

  • PM2(pm2 start app.js -i max
  • 或 K8s / Docker 级别的多实例。

8. 部署与运行时配置

8.1 必备配置

  • NODE_ENV=production

    • 关闭部分调试逻辑
    • 某些框架在此模式下做性能优化(如禁用 dev 中间件)
  • 日志输出:

    • 控制日志级别(避免每个请求都打大量 debug log)
    • 使用 JSON 日志方便采集
  • 超时控制:

    • HTTP 请求超时
    • 数据库查询超时
    • Redis 操作超时

8.2 负载均衡

  • 在 Node 前面加一层负载均衡:

    • Nginx / Envoy / 云负载均衡
  • 主要负责:

    • TLS 终结
    • 静态资源缓存
    • 多实例请求分发

9. 性能调优 Checklist(可直接贴在团队 Wiki)

9.1 观测与测量

  • 是否有基础压测(QPS / 延迟)?
  • 是否有 CPU / 内存监控?
  • 是否测量过事件循环延迟(event loop lag)?
  • 是否做过一次完整 CPU Profile?

9.2 事件循环与阻塞

  • 是否存在同步 I/O 调用?
  • 是否有明显的 while / for 重计算逻辑?
  • 是否处理过单次超大 JSON 操作?
  • 是否考虑将重任务切片 / 下放到 Worker?

9.3 I/O 与数据库

  • 是否使用数据库连接池?
  • 是否检查过 N+1 查询问题?
  • 是否为常用查询建立索引?
  • 是否使用 HTTP keep-alive?

9.4 CPU 与 Worker Threads

  • 是否存在明显 CPU 密集型逻辑?
  • 这些逻辑是否可以迁移到 Worker Threads?
  • 或者拆分为独立微服务?

9.5 内存与 GC

  • 是否监控 Heap 使用情况?
  • 是否使用 Heap Snapshot 排查过泄漏?
  • 是否有无上限的缓存结构?

9.6 多进程与部署

  • 是否充分利用多核?
  • 是否使用 PM2 / K8s 等方式管理多实例?
  • 是否有健康检查与自动重启机制?

结语

Node.js 性能调优,本质上是在以下三件事之间不断迭代:

  1. 看清瓶颈(测量 & Profiling)
  2. 改进架构(I/O 模型、任务拆分、多进程、多服务)
  3. 修正实现细节(避免 sync、控制缓存、减少日志开销)

只要掌握了事件循环、线程池和 I/O 模型这三块核心积木,剩下的就是不断调整系统形状,让它更贴合业务和硬件的约束。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页