后端框架(01):Node.js 与 NestJS——事件循环、libuv、NestJS 模块化、性能陷阱
更新时间:2026-09-01。本文是
backend/framework/后端框架第 01 篇,在 框架索引下。Node.js 是 V8 引擎驱动的 JavaScript 运行时,单线程事件循环 + libuv 线程池。NestJS 是基于 Express/Fastify 的模块化框架,支持依赖注入、守卫、拦截器、管道。理解事件循环和异步模型,才能写出高性能的 Node.js 服务。
本文要回答的问题
- Node.js 事件循环是什么?libuv 线程池什么时候用?
- 为什么 Node.js 单线程还能高并发?
- NestJS 的依赖注入和模块化怎么设计?
- 常见性能陷阱有哪些?怎么避免?
一、Node.js 异步模型
Node.js 架构:
┌─────────────┐
│ V8 引擎 │
└─────────────┘
│ JS 堆内存 │
└─────────────┘
│
┌─────────────┐
│ libuv │
├─────────────┤
│ 事件循环 │ ← 处理 I/O 事件
│ 线程池 │ ← 阻塞 I/O、CPU 密集
│ TCP/UDP 套接字│
└─────────────┘核心思想:
- 单线程 JS:JS 代码在主线程运行,单线程执行
- 异步 I/O:阻塞操作交给 libuv 线程池,完成后回调主线程
- 非阻塞 I/O:网络请求直接返回,回调在事件循环处理
二、事件循环阶段
┌──────────────────────┐
│ timers │ ← 处理 setTimeout/setInterval
├──────────────────────┤
│ pending callbacks │ ← 处理 I/O 回调(大部分 I/O)
├──────────────────────┤
│ idle/prepare │ ← 内部使用
├──────────────────────┤
│ poll │ ← 处理新的 I/O 事件
├──────────────────────┤
│ check │ ← 执行 setImmediate 回调
├──────────────────────┤
│ close callbacks │ ← 处理 close 事件
└──────────────────────┘
└──→ 回到 poll关键点:
setTimeout在 timers 阶段执行setImmediate在 check 阶段执行- I/O 回调除了关闭事件,都在 pending callbacks 阶段
- poll 阶段:检查是否有 I/O 事件,如果没有事件,阻塞在这里
三、libuv 线程池
什么时候用线程池:
- 阻塞 I/O:文件系统操作(
fs.readFile) - CPU 密集:
crypto加密、zlib 压缩 - DNS 查询:
dns.lookup
javascript
// 默认线程池大小:4
// 修改大小:UV_THREADPOOL_SIZE=16 node app.js
// 线程池大小最大 1024
const crypto = require('crypto');
// 加密操作会使用 libuv 线程池
const hash = crypto.pbkdf2Sync('password', 'salt', 100000, 512, 'sha256');
// 如果线程池满了,后续任务会排队四、NestJS 模块化和依赖注入
typescript
// NestJS 模块:按功能划分
// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [UsersModule, OrdersModule],
})
export class AppModule {}
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
providers: [UsersService], // 注册服务
exports: [UsersService], // 导出给其他模块用
})
export class UsersModule {}
// 依赖注入:构造函数注入
@Controller()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// 直接使用 this.usersService
}NestJS 核心概念:
@Module:模块,组织功能单元@Controller:控制器,处理请求路由@Injectable:可注入服务,业务逻辑@Guard:守卫,权限校验@Interceptor:拦截器,日志、请求跟踪@Pipe:管道,参数校验、转换
五、Node.js 常见性能陷阱
1. CPU 密集阻塞事件循环
javascript
// ❌ 在主线程做大量计算,阻塞事件循环,所有请求变慢
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
app.get('/fib', (req, res) => {
const result = fib(50); // 阻塞主线程,其他请求等计算完成
res.json({ result });
});
// ✅ 交给 worker threads 或放到线程池
const { Worker } = require('worker_threads');
app.get('/fib', async (req, res) => {
const worker = new Worker('./fib.js');
worker.postMessage(50);
worker.on('message', (result) => {
res.json({ result });
worker.terminate();
});
});2. 事件循环阻塞在同步操作
javascript
// ❌ 同步读取大文件,阻塞事件循环
const data = fs.readFileSync('/path/to/large-file.txt');
// ✅ 异步读取
const data = await fs.promises.readFile('/path/to/large-file.txt');3. libuv 线程池溢出
javascript
// 所有文件系统操作都用线程池
// 如果同时读很多大文件,线程池满了,任务排队
// 解决:增加 UV_THREADPOOL_SIZE,或者用流处理4. 内存泄漏
javascript
// ❌ 全局缓存增长不清理
global.cache = {};
app.get('/cache', (req, res) => {
global.cache[req.query.key] = req.query.value; // 一直增长不清理
});
// ✅ 用 LRU cache 限制大小
const lru = new LRUCache({ max: 1000 });六、Node.js 性能优化
javascript
// 1. 用 cluster 多核利用
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork(); // 每个核一个进程
}
} else {
require('./app'); // 子进程启动服务
}
// 2. 用 Fastify 替代 Express
// Fastify 比 Express 快 2-5x,JSON 解析更快
// 3. 静态文件用 CDN 或 nginx 代理,不要让 Node.js 处理
// 4. 开启 gzip 压缩,减少响应体积
// 5. 连接池复用数据库连接,不要每次新建连接七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| CPU 密集阻塞主线程 | 所有请求变慢,延迟高 | 交给 worker threads 或外部进程 |
| 不监控内存泄漏 | 内存一直增长,最后 OOM | 用 clinic.js 分析,限制缓存大小 |
| libuv 线程池太小 | 文件系统操作排队 | 增加 UV_THREADPOOL_SIZE |
| 同步操作阻塞 | 所有请求等同步操作完成 | 用异步 API |
相关与延伸
下一篇:Spring Boot——启动原理、自动装配、WebFlux 响应式、调优;后端框架索引见 index.md。
一句话总结
Node.js 与 NestJS:Node.js 单线程事件循环 + libuv 线程池,阻塞 I/O 和 CPU 密集交给线程池;事件循环分多个阶段(timers/poll/check),I/O 回调依次处理;NestJS 模块化划分功能,依赖注入构造函数注入,守卫/拦截器/管道做切面处理;常见陷阱:CPU 密集阻塞主线程,交给 worker threads;内存泄漏需要限制缓存大小;多核用 cluster 多进程,用 Fastify 比 Express 快;静态文件让 nginx 处理,不要让 Node.js 处理。