性能优化
一、性能优化原则
先测量,后优化。 不要凭直觉优化——先用工具找到真正的瓶颈:
定位瓶颈的工具:
- clinic.js:Node.js 专用火焰图分析
- Artillery / k6:负载测试
- pg_stat_statements:PostgreSQL 慢查询统计
- Redis SLOWLOG:Redis 慢命令
- NestJS 请求日志:接口响应时间常见瓶颈排序(按出现频率):
- 数据库查询(N+1、缺少索引、大量 JOIN)
- 无缓存的热点数据
- 同步阻塞操作(CPU 密集型任务在主线程)
- 内存泄漏(未释放的监听器、循环引用)
- HTTP 框架(Express vs Fastify)
二、切换为 Fastify
Fastify 比 Express 快约 2-3x,适合高 QPS API:
bash
npm install @nestjs/platform-fastifytypescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: false, // 使用 NestJS 日志系统
trustProxy: true, // 信任代理(nginx 后面时需要)
bodyLimit: 10485760, // 10MB 请求体限制
}),
);
// Fastify 中 helmet 和 compression 的安装方式不同
await app.register(require('@fastify/helmet'));
await app.register(require('@fastify/compress'));
await app.listen(3000, '0.0.0.0');
}⚠️ 迁移注意事项:
res.setHeader()→res.header()req.ip获取方式可能不同- 某些 Express 中间件与 Fastify 不兼容,需要找对应的 Fastify 插件
三、响应缓存
基础内存缓存
typescript
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
@Module({
imports: [
CacheModule.register({
isGlobal: true,
ttl: 60 * 1000, // 默认 60 秒(ms)
max: 1000, // 最多缓存 1000 条
}),
],
})typescript
// Controller 级别缓存
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
@UseInterceptors(CacheInterceptor) // 自动以 URL 为 key 缓存
@Controller('posts')
export class PostsController {
@Get()
@CacheTTL(30 * 1000) // 30 秒(覆盖默认值)
findAll() {
return this.postsService.findAll();
}
@Get('popular')
@CacheKey('popular-posts') // 自定义缓存 key
@CacheTTL(5 * 60 * 1000) // 热门文章缓存 5 分钟
findPopular() {
return this.postsService.findPopular();
}
}Redis 缓存(多实例必须用)
bash
npm install @nestjs/cache-manager cache-manager-redis-yettypescript
CacheModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
store: redisStore,
url: config.get('REDIS_URL', 'redis://localhost:6379'),
ttl: 60 * 1000,
}),
})手动缓存控制(更精细)
typescript
@Injectable()
export class PostsService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async findOne(id: number): Promise<Post> {
const cacheKey = `post:${id}`;
const cached = await this.cacheManager.get<Post>(cacheKey);
if (cached) return cached;
const post = await this.postRepo.findOne({ where: { id } });
if (!post) throw new NotFoundException();
await this.cacheManager.set(cacheKey, post, 5 * 60 * 1000);
return post;
}
async update(id: number, dto: UpdatePostDto) {
const post = await this.postRepo.save({ id, ...dto });
// 更新时清除缓存(缓存失效策略)
await this.cacheManager.del(`post:${id}`);
return post;
}
async invalidateUserCache(userId: number) {
// 批量清除某用户相关的所有缓存
const keys = await this.cacheManager.store.keys(`user:${userId}:*`);
await Promise.all(keys.map(k => this.cacheManager.del(k)));
}
}四、数据库查询优化
只查需要的字段
typescript
// ❌ 查询所有字段(包括大文本字段)
const posts = await this.postRepo.find();
// ✅ 只查列表页需要的字段
const posts = await this.postRepo.find({
select: {
id: true,
title: true,
authorId: true,
createdAt: true,
// content 不查(可能很大)
},
relations: { author: true },
});避免 N+1
typescript
// ❌ N+1:查 10 篇文章,再查 10 次作者
const posts = await this.postRepo.find();
for (const post of posts) {
const author = await this.userRepo.findOne({ where: { id: post.authorId } });
}
// ✅ 一次 JOIN 查询
const posts = await this.postRepo.find({
relations: { author: true },
});
// ✅ QueryBuilder 选择性字段 JOIN
const posts = await this.postRepo
.createQueryBuilder('post')
.leftJoin('post.author', 'author')
.addSelect(['author.id', 'author.name']) // 不查 author.password 等
.getMany();分页最佳实践
typescript
// ❌ 大偏移量分页(OFFSET 1000000 全表扫描)
const posts = await this.postRepo.find({
skip: 100000,
take: 10,
});
// ✅ 游标分页(基于最后一条记录的 ID 跳转)
async findNextPage(lastId: number, limit: number = 10) {
return this.postRepo
.createQueryBuilder('post')
.where('post.id > :lastId', { lastId })
.orderBy('post.id', 'ASC')
.take(limit)
.getMany();
}索引策略
typescript
@Entity('posts')
@Index(['authorId', 'createdAt']) // 复合索引:按作者分页查询
@Index(['title'], { fulltext: true }) // 全文索引(MySQL)
export class Post {
@Column()
@Index() // 单列索引:频繁过滤的字段
published: boolean;
@Column()
authorId: number;
}五、异步与并发优化
并行请求(Promise.all)
typescript
// ❌ 串行:总耗时 = 100ms + 200ms + 150ms = 450ms
const user = await this.usersService.findOne(id); // 100ms
const posts = await this.postsService.findByUser(id); // 200ms
const stats = await this.statsService.getUserStats(id); // 150ms
// ✅ 并行:总耗时 = max(100, 200, 150) = 200ms
const [user, posts, stats] = await Promise.all([
this.usersService.findOne(id),
this.postsService.findByUser(id),
this.statsService.getUserStats(id),
]);
// 允许部分失败(不影响其他结果)
const results = await Promise.allSettled([...]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;CPU 密集型任务外包
typescript
// ❌ 在主线程处理大图片(会阻塞所有请求)
async processImage(buffer: Buffer) {
return sharp(buffer).resize(800).jpeg().toBuffer(); // CPU 密集
}
// ✅ 用 Worker Thread 或独立进程处理
import { Worker } from 'worker_threads';
async processImageAsync(buffer: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
const worker = new Worker('./image-worker.js', {
workerData: { buffer },
});
worker.on('message', resolve);
worker.on('error', reject);
});
}六、限流与熔断
typescript
// @nestjs/throttler 全局配置(见安全实践章节详细说明)
ThrottlerModule.forRoot([{
ttl: 60000,
limit: 100,
}])连接池配置
typescript
// TypeORM 连接池
TypeOrmModule.forRoot({
extra: {
max: parseInt(process.env.DB_POOL_MAX ?? '10'), // 最大连接数
min: 2,
acquireTimeoutMillis: 30000, // 等待连接的最大时间
idleTimeoutMillis: 600000, // 空闲连接超时
},
})
// Redis 连接池(ioredis)
new Redis({
maxRetriesPerRequest: 3,
connectTimeout: 5000,
lazyConnect: true, // 延迟连接(启动时不阻塞)
enableReadyCheck: false,
})七、性能监控
typescript
// 自定义指标拦截器(上报响应时间到监控系统)
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const start = process.hrtime.bigint();
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => {
const duration = Number(process.hrtime.bigint() - start) / 1e6; // 纳秒 → 毫秒
const route = `${req.method} ${req.route?.path ?? req.url}`;
// 记录 P50/P95/P99(在真实项目中上报到 Prometheus/Datadog)
if (duration > 1000) {
console.warn(`慢接口: ${route} ${duration.toFixed(2)}ms`);
}
}),
);
}
}八、性能优化检查清单
数据库层:
☐ 高频查询字段有索引
☐ 分页查询有限制(TAKE < 100)
☐ 无 N+1 问题(检查 TypeORM 日志)
☐ 只 SELECT 需要的字段
☐ 连接池大小与并发量匹配
缓存层:
☐ 热点只读数据有缓存
☐ 缓存有合理 TTL(不是永不过期)
☐ 写操作时同步清除缓存
☐ 多实例用 Redis 而非内存缓存
应用层:
☐ 无关操作并行(Promise.all)
☐ CPU 密集型任务用 Worker Thread
☐ 大文件流式响应(不全加载到内存)
☐ 响应开启 gzip 压缩可运行 Demo:
practice/07-extensions— 缓存 + 队列异步化 + DataLoader N+1 优化示例
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| 缓存雪崩 | 大量 key 同时过期触发 DB 查询风暴 | TTL 加随机抖动:baseTtl + Math.random() * 0.2 * baseTtl |
| N+1 查询 | 循环中单独查每条关联数据 | DataLoader 批处理,或 JOIN 一次性加载 |
| 内存泄漏 | 全局 Map/Set 无限增长 | 用 LRU Cache 限制大小;REQUEST 作用域的服务在请求结束后会被 GC |
| Fastify 与某些 Express 中间件不兼容 | Fastify 不使用 Express 中间件接口 | 迁移到 Fastify 前检查依赖的中间件是否有 Fastify 版本 |