NestJSはデフォルトでExpressを使用しますが、高負荷環境ではパフォーマンスがボトルネックになることがあります。本記事では、Fastifyアダプターへの移行による高速化、効果的なキャッシュ戦略の実装、レスポンス圧縮の設定、そしてパフォーマンス計測と監視の手法を解説します。これらのテクニックを組み合わせることで、本番環境で安定して高負荷に耐えるアプリケーションを構築できます。
前提条件#
本記事の内容を実践するには、以下の環境が必要です。
| 項目 |
バージョン・条件 |
| Node.js |
20.x 以上 |
| NestJS |
11.x |
| TypeScript |
5.x |
| npm または yarn |
最新版 |
| OS |
Windows / macOS / Linux |
事前に以下の準備を完了してください。
- NestJSプロジェクトの作成済み
- NestJSの基本的なアーキテクチャ(Module、Controller、Provider)の理解
@nestjs/configによる環境変数管理の基本理解
NestJSプロジェクトの作成方法はNestJS入門記事を、環境変数管理は@nestjs/configによる環境設定管理を参照してください。
ExpressとFastifyの比較#
NestJSは「プラットフォーム非依存」のフレームワークであり、HTTPプロバイダーとしてExpressまたはFastifyを選択できます。
パフォーマンス比較#
| 項目 |
Express |
Fastify |
| リクエスト/秒 |
約15,000 |
約30,000 |
| レイテンシ |
約6.5ms |
約3.2ms |
| メモリ使用量 |
標準 |
若干少ない |
| エコシステム |
非常に豊富 |
成長中 |
Fastifyは、ベンチマーク結果でExpressの約2倍のパフォーマンスを発揮します。これはFastifyが、JSONスキーマによるシリアライゼーション最適化、効率的なルーティングアルゴリズム、そしてプラグインベースのアーキテクチャを採用しているためです。
移行時の考慮事項#
Fastifyへ移行する際には以下の点に注意してください。
- ミドルウェアの互換性: Express用のミドルウェアはそのままでは動作しません
- リクエスト・レスポンスオブジェクト: Fastify固有のAPIを使用する必要があります
- ファイルアップロード:
@fastify/multipartなど専用パッケージが必要です
FastifyAdapterによる高性能化#
パッケージのインストール#
まず、Fastifyアダプターをインストールします。
1
|
npm install @nestjs/platform-fastify
|
main.tsの設定#
ExpressからFastifyに切り替えるには、main.tsを以下のように変更します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
// FastifyAdapterを使用してアプリケーションを作成
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
// Fastifyはデフォルトでlocalhost(127.0.0.1)のみをリッスン
// 外部からのアクセスを許可する場合は'0.0.0.0'を指定
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
|
FastifyAdapterのオプション設定#
FastifyAdapterのコンストラクタにオプションを渡すことで、Fastifyの詳細設定を行えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
// ロギングを有効化
logger: true,
// リクエストボディの最大サイズ(デフォルト: 1MB)
bodyLimit: 10485760, // 10MB
// 信頼するプロキシの設定
trustProxy: true,
}),
);
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}
bootstrap();
|
Fastify用ミドルウェアの実装#
Fastifyを使用する場合、ミドルウェアはFastifyRequestとFastifyReplyの生オブジェクトを受け取ります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// src/common/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(
req: FastifyRequest['raw'],
res: FastifyReply['raw'],
next: () => void,
): void {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} - ${duration}ms`);
});
next();
}
}
|
ルート設定とカスタムデコレータ#
Fastifyでは@RouteConfig()と@RouteConstraints()デコレータを使用して、ルートごとの設定を行えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// src/users/users.controller.ts
import { Controller, Get, Req } from '@nestjs/common';
import { RouteConfig, RouteConstraints } from '@nestjs/platform-fastify';
import { FastifyRequest } from 'fastify';
@Controller('users')
export class UsersController {
@Get()
@RouteConfig({ output: 'users list' })
findAll(@Req() req: FastifyRequest) {
// routeConfigにアクセス可能
console.log(req.routeOptions.config);
return [];
}
@Get('v2')
@RouteConstraints({ version: '2.x' })
findAllV2() {
// バージョン2.x向けのエンドポイント
return { version: 2, users: [] };
}
}
|
CacheModuleによるキャッシュ実装#
キャッシュは頻繁にアクセスされるデータを一時的に保存し、データベースクエリや外部APIへのリクエストを削減することでパフォーマンスを向上させます。
パッケージのインストール#
1
|
npm install @nestjs/cache-manager cache-manager
|
インメモリキャッシュの設定#
最もシンプルなインメモリキャッシュの設定を行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
CacheModule.register({
// キャッシュの有効期限(ミリ秒)
ttl: 5000,
// 最大エントリ数
max: 100,
// グローバルモジュールとして登録
isGlobal: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
|
Serviceでのキャッシュ利用#
CACHE_MANAGERトークンを使用してキャッシュマネージャーをインジェクトします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
// src/products/products.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
interface Product {
id: number;
name: string;
price: number;
}
@Injectable()
export class ProductsService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async findAll(): Promise<Product[]> {
const cacheKey = 'products:all';
// キャッシュから取得を試みる
const cached = await this.cacheManager.get<Product[]>(cacheKey);
if (cached) {
console.log('Cache hit: products:all');
return cached;
}
console.log('Cache miss: products:all');
// データベースからデータを取得(実際の実装では Repository を使用)
const products: Product[] = await this.fetchProductsFromDatabase();
// キャッシュに保存(TTL: 60秒)
await this.cacheManager.set(cacheKey, products, 60000);
return products;
}
async findOne(id: number): Promise<Product | null> {
const cacheKey = `products:${id}`;
const cached = await this.cacheManager.get<Product>(cacheKey);
if (cached) {
return cached;
}
const product = await this.fetchProductFromDatabase(id);
if (product) {
await this.cacheManager.set(cacheKey, product, 60000);
}
return product;
}
async update(id: number, data: Partial<Product>): Promise<Product> {
const product = await this.updateProductInDatabase(id, data);
// 更新時はキャッシュを削除
await this.cacheManager.del(`products:${id}`);
await this.cacheManager.del('products:all');
return product;
}
async clearCache(): Promise<void> {
// キャッシュ全体をクリア
await this.cacheManager.clear();
}
private async fetchProductsFromDatabase(): Promise<Product[]> {
// 実際のデータベースクエリをシミュレート
return [
{ id: 1, name: 'Product A', price: 1000 },
{ id: 2, name: 'Product B', price: 2000 },
];
}
private async fetchProductFromDatabase(id: number): Promise<Product | null> {
const products = await this.fetchProductsFromDatabase();
return products.find((p) => p.id === id) ?? null;
}
private async updateProductInDatabase(
id: number,
data: Partial<Product>,
): Promise<Product> {
return { id, name: data.name ?? 'Updated', price: data.price ?? 0 };
}
}
|
CacheInterceptorによる自動キャッシュ#
CacheInterceptorを使用すると、GETエンドポイントのレスポンスを自動的にキャッシュできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// src/products/products.controller.ts
import {
Controller,
Get,
Param,
ParseIntPipe,
UseInterceptors,
} from '@nestjs/common';
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
import { ProductsService } from './products.service';
@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Get()
@CacheTTL(30000) // 30秒
@CacheKey('all-products')
findAll() {
return this.productsService.findAll();
}
@Get(':id')
@CacheTTL(60000) // 60秒
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}
}
|
グローバルCacheInterceptorの設定#
アプリケーション全体でキャッシュを有効にするには、APP_INTERCEPTORを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';
@Module({
imports: [
CacheModule.register({
ttl: 5000,
isGlobal: true,
}),
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: CacheInterceptor,
},
],
})
export class AppModule {}
|
Redisキャッシュの設定#
本番環境では、スケーラブルなRedisキャッシュを使用することを推奨します。
1
|
npm install @keyv/redis keyv cacheable
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule, ConfigService } from '@nestjs/config';
import KeyvRedis from '@keyv/redis';
import { Keyv } from 'keyv';
import { CacheableMemory } from 'cacheable';
@Module({
imports: [
ConfigModule.forRoot(),
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
const redisUrl = configService.get<string>('REDIS_URL');
return {
stores: [
// L1キャッシュ: インメモリ(高速)
new Keyv({
store: new CacheableMemory({
ttl: 60000,
lruSize: 5000,
}),
}),
// L2キャッシュ: Redis(永続化・共有)
new KeyvRedis(redisUrl ?? 'redis://localhost:6379'),
],
};
},
inject: [ConfigService],
}),
],
})
export class AppModule {}
|
カスタムCacheInterceptorの実装#
認証情報に基づいてキャッシュキーを生成するカスタムInterceptorを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// src/common/interceptors/http-cache.interceptor.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { FastifyRequest } from 'fastify';
@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext): string | undefined {
const request = context.switchToHttp().getRequest<FastifyRequest>();
// POSTやPUT等はキャッシュしない
if (request.method !== 'GET') {
return undefined;
}
// ユーザーごとにキャッシュを分離
const userId = (request as any).user?.id ?? 'anonymous';
const url = request.url;
return `${userId}:${url}`;
}
}
|
レスポンス圧縮の設定#
レスポンス圧縮は、転送データ量を削減してパフォーマンスを向上させます。
Fastify用圧縮パッケージのインストール#
1
|
npm install @fastify/compress
|
圧縮の設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import compression from '@fastify/compress';
import { constants } from 'node:zlib';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
// 圧縮ミドルウェアを登録
await app.register(compression, {
// Brotli圧縮の品質設定(0-11、デフォルト: 11)
// 値が低いほど高速だが圧縮率は低下
brotliOptions: {
params: {
[constants.BROTLI_PARAM_QUALITY]: 4,
},
},
// 圧縮の優先順位(Brotli > gzip > deflate)
encodings: ['br', 'gzip', 'deflate'],
// 閾値(このサイズ以下のレスポンスは圧縮しない)
threshold: 1024, // 1KB
});
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}
bootstrap();
|
圧縮のエンコーディング設定#
高速レスポンスを優先する場合は、gzipとdeflateのみを使用します。
1
2
3
4
5
6
|
// src/main.ts
await app.register(compression, {
// Brotliを使用せずgzipとdeflateのみを使用
// レスポンスサイズは増加するが配信速度は向上
encodings: ['gzip', 'deflate'],
});
|
本番環境での推奨設定#
高トラフィックの本番環境では、アプリケーションサーバーではなくリバースプロキシ(Nginx等)で圧縮を行うことを推奨します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# nginx.conf
http {
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
gzip_min_length 1024;
# Brotli(ngx_brotli モジュールが必要)
brotli on;
brotli_comp_level 4;
brotli_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}
|
パフォーマンス計測と監視#
パフォーマンスチューニングには、計測と監視が不可欠です。
レスポンスタイム計測Interceptor#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
// src/common/interceptors/performance.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { FastifyRequest } from 'fastify';
interface PerformanceMetrics {
method: string;
url: string;
statusCode: number;
duration: number;
timestamp: Date;
}
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
private readonly logger = new Logger(PerformanceInterceptor.name);
private readonly metrics: PerformanceMetrics[] = [];
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const response = context.switchToHttp().getResponse();
const { method, url } = request;
const startTime = Date.now();
return next.handle().pipe(
tap({
next: () => {
const duration = Date.now() - startTime;
const statusCode = response.statusCode;
// 警告閾値(500ms以上)
if (duration > 500) {
this.logger.warn(
`Slow request: ${method} ${url} - ${duration}ms`,
);
}
// メトリクスを記録
this.metrics.push({
method,
url,
statusCode,
duration,
timestamp: new Date(),
});
// メトリクス数を制限(直近1000件)
if (this.metrics.length > 1000) {
this.metrics.shift();
}
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(
`Error request: ${method} ${url} - ${duration}ms - ${error.message}`,
);
},
}),
);
}
getMetrics(): PerformanceMetrics[] {
return [...this.metrics];
}
getAverageResponseTime(): number {
if (this.metrics.length === 0) return 0;
const total = this.metrics.reduce((sum, m) => sum + m.duration, 0);
return total / this.metrics.length;
}
}
|
メトリクスエンドポイントの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
// src/metrics/metrics.controller.ts
import { Controller, Get } from '@nestjs/common';
import { PerformanceInterceptor } from '../common/interceptors/performance.interceptor';
interface MetricsSummary {
totalRequests: number;
averageResponseTime: number;
slowRequests: number;
errorRate: number;
uptime: number;
memoryUsage: NodeJS.MemoryUsage;
}
@Controller('metrics')
export class MetricsController {
private readonly startTime = Date.now();
constructor(
private readonly performanceInterceptor: PerformanceInterceptor,
) {}
@Get()
getMetrics(): MetricsSummary {
const metrics = this.performanceInterceptor.getMetrics();
const slowRequests = metrics.filter((m) => m.duration > 500).length;
const errorRequests = metrics.filter((m) => m.statusCode >= 400).length;
return {
totalRequests: metrics.length,
averageResponseTime: this.performanceInterceptor.getAverageResponseTime(),
slowRequests,
errorRate: metrics.length > 0 ? errorRequests / metrics.length : 0,
uptime: Date.now() - this.startTime,
memoryUsage: process.memoryUsage(),
};
}
@Get('health')
healthCheck() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
}
}
|
メモリ使用量の監視#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
// src/common/services/memory-monitor.service.ts
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
interface MemorySnapshot {
timestamp: Date;
heapUsed: number;
heapTotal: number;
external: number;
rss: number;
}
@Injectable()
export class MemoryMonitorService implements OnModuleInit {
private readonly logger = new Logger(MemoryMonitorService.name);
private readonly snapshots: MemorySnapshot[] = [];
private intervalId: NodeJS.Timeout;
onModuleInit() {
// 30秒ごとにメモリ使用量を記録
this.intervalId = setInterval(() => {
this.recordSnapshot();
}, 30000);
}
onModuleDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
private recordSnapshot(): void {
const memoryUsage = process.memoryUsage();
const snapshot: MemorySnapshot = {
timestamp: new Date(),
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
external: memoryUsage.external,
rss: memoryUsage.rss,
};
this.snapshots.push(snapshot);
// 直近100件のスナップショットを保持
if (this.snapshots.length > 100) {
this.snapshots.shift();
}
// ヒープ使用率が80%を超えた場合に警告
const heapUsagePercent = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
if (heapUsagePercent > 80) {
this.logger.warn(
`High memory usage: ${heapUsagePercent.toFixed(2)}% (${this.formatBytes(memoryUsage.heapUsed)} / ${this.formatBytes(memoryUsage.heapTotal)})`,
);
}
}
getSnapshots(): MemorySnapshot[] {
return [...this.snapshots];
}
getCurrentUsage(): MemorySnapshot {
const memoryUsage = process.memoryUsage();
return {
timestamp: new Date(),
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
external: memoryUsage.external,
rss: memoryUsage.rss,
};
}
private formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let unitIndex = 0;
let value = bytes;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(2)} ${units[unitIndex]}`;
}
}
|
パフォーマンス最適化のベストプラクティス#
データベースクエリの最適化#
キャッシュと組み合わせて、N+1問題を回避します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
// src/products/products.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
@Injectable()
export class ProductsService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
@InjectRepository(Product)
private productRepository: Repository<Product>,
) {}
async findAllWithCategories(): Promise<Product[]> {
const cacheKey = 'products:with-categories';
const cached = await this.cacheManager.get<Product[]>(cacheKey);
if (cached) {
return cached;
}
// N+1問題を回避するJOINクエリ
const products = await this.productRepository
.createQueryBuilder('product')
.leftJoinAndSelect('product.category', 'category')
.leftJoinAndSelect('product.reviews', 'reviews')
.orderBy('product.createdAt', 'DESC')
.getMany();
await this.cacheManager.set(cacheKey, products, 60000);
return products;
}
}
|
接続プールの設定#
データベース接続プールを適切に設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
autoLoadEntities: true,
// 接続プール設定
extra: {
// 最大接続数
max: 20,
// 接続タイムアウト(ミリ秒)
connectionTimeoutMillis: 5000,
// アイドル接続のタイムアウト(ミリ秒)
idleTimeoutMillis: 30000,
},
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
|
完全な構成例#
最終的なmain.tsの完全な構成例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import compression from '@fastify/compress';
import { constants } from 'node:zlib';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
import { PerformanceInterceptor } from './common/interceptors/performance.interceptor';
async function bootstrap() {
const logger = new Logger('Bootstrap');
// FastifyAdapterで高速なHTTPサーバーを構築
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: true,
bodyLimit: 10485760, // 10MB
trustProxy: true,
}),
);
const configService = app.get(ConfigService);
// レスポンス圧縮
await app.register(compression, {
brotliOptions: {
params: {
[constants.BROTLI_PARAM_QUALITY]: 4,
},
},
encodings: ['br', 'gzip', 'deflate'],
threshold: 1024,
});
// グローバルバリデーション
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
// パフォーマンス計測
const performanceInterceptor = app.get(PerformanceInterceptor);
app.useGlobalInterceptors(performanceInterceptor);
// CORS設定
app.enableCors({
origin: configService.get<string>('CORS_ORIGIN', '*'),
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
credentials: true,
});
// グレースフルシャットダウン
app.enableShutdownHooks();
const port = configService.get<number>('PORT', 3000);
await app.listen(port, '0.0.0.0');
logger.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
|
パフォーマンス改善の効果測定#
以下のような改善効果が期待できます。
graph LR
A[Express] -->|ベースライン| B[15,000 req/s]
C[Fastify] -->|+100%| D[30,000 req/s]
E[+キャッシュ] -->|+200%| F[45,000 req/s]
G[+圧縮] -->|転送量-70%| H[帯域節約]
| 最適化項目 |
改善効果 |
| FastifyAdapter |
スループット約2倍 |
| インメモリキャッシュ |
DB負荷50-90%削減 |
| Redisキャッシュ |
スケールアウト対応 |
| レスポンス圧縮 |
転送量60-80%削減 |
| 接続プール最適化 |
接続オーバーヘッド削減 |
まとめ#
本記事では、NestJSアプリケーションのパフォーマンスチューニングについて解説しました。
- FastifyAdapter: Expressから切り替えることで約2倍のスループット向上
- CacheModule: インメモリキャッシュとRedisキャッシュによるDB負荷削減
- レスポンス圧縮: Brotli/gzipによる転送データ量の大幅削減
- パフォーマンス監視: 計測とモニタリングによる継続的な改善
これらのテクニックを適切に組み合わせることで、高負荷環境でも安定して動作するNestJSアプリケーションを構築できます。本番環境への適用前には、必ず負荷テストを実施してボトルネックを特定し、段階的に最適化を進めてください。
参考リンク#