ExpressJS (TypeScript) – 效能優化
主題:避免不必要的 Middleware 啟動
簡介
在使用 Express 開發 API 時,我們常會把各式各樣的 middleware 堆疊在一起,從驗證、日誌、壓縮到錯誤處理,功能上非常便利。但每一次請求都會依序走過所有已註冊的 middleware,若其中有不需要在特定路由或請求類型上執行的程式碼,就會造成 不必要的 CPU、記憶體與 I/O 開銷,最終影響整體效能與延遲。
本篇文章將說明 「避免不必要 middleware 啟動」 的核心概念,提供實作範例、常見陷阱與最佳實踐,幫助你在 Express + TypeScript 專案中,既保留彈性又能維持高效能。
核心概念
1. Middleware 的執行機制
Express 會依照 註冊順序 逐一呼叫每個 middleware,除非 middleware 主動呼叫 next() 或直接回傳回應。若某個 middleware 在不需要的情況下仍被呼叫,就會浪費資源。
重點:只有在符合條件的請求才應該讓特定 middleware 參與。
2. 路由層級與全域層級的差別
- 全域 (app.use):每個請求都會經過。適合日誌、CORS、body‑parser 等「所有請求」都需要的功能。
- 路由層級 (router.use / route-specific middleware):只在匹配到該路由時才會執行。適合驗證、授權、檔案上傳等與特定路徑相關的功能。
建議:將 「僅在少數路由使用」 的 middleware 從全域搬到路由層級。
3. 條件式載入 Middleware
有時候同一個 middleware 只在 特定 HTTP 方法、特定 Content‑Type 或 特定環境 下才需要。利用條件判斷或自訂 wrapper 可讓 middleware 只在需要時執行。
程式碼範例
以下範例均使用 TypeScript,搭配 Express 5.x(支援 app.all、router.route)撰寫。
範例 1️⃣:把全域的驗證 middleware 改為路由層級
import express, { Request, Response, NextFunction } from 'express';
import { verifyToken } from './auth';
const app = express();
// ❌ 全域載入會導致每個請求都執行驗證
// app.use(verifyToken);
// ✅ 只在需要驗證的路由上使用
const protectedRouter = express.Router();
protectedRouter.use(verifyToken); // 只在此 router 內的路由觸發
protectedRouter.get('/profile', (req: Request, res: Response) => {
res.json({ user: req.user });
});
app.use('/api', protectedRouter);
說明:
verifyToken只在/api/*需要驗證的路由上執行,避免對公開資源(如首頁、健康檢查)造成額外負擔。
範例 2️⃣:條件式啟用壓縮 middleware(只對 JSON 回應)
import compression from 'compression';
import { json, Request, Response, NextFunction } from 'express';
const app = express();
// 只在回傳 JSON 時才壓縮
function compressIfJson(req: Request, res: Response, next: NextFunction) {
// 先掛上原始的 writeHead,以便之後檢查 Content-Type
const originalWriteHead = res.writeHead;
res.writeHead = function (statusCode, statusMessage, headers) {
const ct = res.getHeader('Content-Type')?.toString() ?? '';
if (ct.includes('application/json')) {
compression()(req, res, () => {}); // 啟動 gzip
}
// 呼叫原本的 writeHead
return originalWriteHead.apply(this, arguments as any);
};
next();
}
app.use(json());
app.use(compressIfJson);
說明:大多數 API 只回傳 JSON,透過
compressIfJson只在需要時才載入compression,減少對靜態檔案或純文字回應的額外處理。
範例 3️⃣:使用 router.use 搭配 HTTP 方法限制
import { Router, Request, Response, NextFunction } from 'express';
const uploadRouter = Router();
// 只在 POST /upload 時啟用檔案上傳 middleware
uploadRouter.post(
'/upload',
(req: Request, res: Response, next: NextFunction) => {
// 在此處載入 multer(懶載入)
const multer = require('multer');
const upload = multer({ dest: 'uploads/' }).single('file');
upload(req, res, next);
},
(req: Request, res: Response) => {
res.json({ filename: req.file?.filename });
}
);
export default uploadRouter;
說明:
multer只在實際需要上傳檔案的 POST 請求時才被require,避免在其他路由上產生不必要的模組載入與初始化成本。
範例 4️⃣:環境變數控制開發/生產日誌
import morgan from 'morgan';
import { Application } from 'express';
export function setupLogger(app: Application) {
if (process.env.NODE_ENV === 'production') {
// 只在 production 使用簡短日誌,減少 I/O
app.use(morgan('combined', { skip: (req, res) => res.statusCode < 400 }));
} else {
// 開發環境使用詳細日誌
app.use(morgan('dev'));
}
}
說明:在生產環境只記錄錯誤請求,減少磁碟寫入;開發環境則保留完整資訊,提升除錯效率。
範例 5️⃣:把昂貴的驗證搬到 API Gateway(概念示範)
此範例示範:當你在微服務架構下,將認證/授權交給前置的 API Gateway,Express 只保留「業務邏輯」層。
// 假設前置 Nginx / Kong 已完成 JWT 驗證,並在 header 加入 X-User-Id
import express, { Request, Response } from 'express';
const app = express();
app.use((req: Request, res: Response, next) => {
const userId = req.header('X-User-Id');
if (!userId) {
return res.status(401).json({ error: 'Unauthenticated' });
}
// 直接把 userId 放入 req 供後續使用
(req as any).userId = userId;
next();
});
app.get('/orders', (req, res) => {
// 這裡不再需要再次驗證 token
res.json({ orders: [], user: (req as any).userId });
});
說明:將重複的驗證工作委派給外部元件,可大幅降低每個服務的 CPU 使用率。
常見陷阱與最佳實踐
| 陷阱 | 可能的影響 | 解決方式 |
|---|---|---|
| 把所有 middleware 註冊為全域 | 每個請求都會走過不必要的程式碼,導致 CPU、記憶體浪費。 | 依路由或條件分層註冊,盡量使用 router.use。 |
在全域 app.use 中寫大量同步計算 |
會阻塞事件迴圈,使其他請求延遲。 | 移到非同步或背景工作,或僅在需要時才執行。 |
忘記在 middleware 結尾呼叫 next() |
請求卡住,最終超時。 | 確認每條路徑都有 next() 或回傳回應。 |
| 在開發環境使用過於詳細的日誌 | 實際部署時忘記關閉,產生大量磁碟 I/O。 | 使用 process.env.NODE_ENV 控制日誌等級。 |
在每次請求內 require 大型模組 |
會重複載入,降低效能。 | 只在需要時懶載入,或在程式啟動時一次載入。 |
最佳實踐總結:
- 先思考「誰需要」,再決定 middleware 的範圍。
- 使用 TypeScript 的型別,在自訂 wrapper 中明確標註
req,res,避免因型別不合而誤用。 - 懶載入(lazy‑load) 大型外部套件,尤其是只在少數路由使用的功能。
- 環境變數切換:開發、測試、正式環境的 middleware 配置應分離。
- 效能監控:使用
response-time、prom-client等工具量測每個 middleware 的耗時,找出瓶頸。
實際應用場景
| 場景 | 建議做法 |
|---|---|
健康檢查 endpoint (/health) |
不掛載任何認證或日誌 middleware,直接回傳 200 OK,確保監控系統的請求最快。 |
大量公開靜態資源 (/public/*) |
使用 express.static,排除所有全域 middleware,只保留必要的 CORS、Cache 控制。 |
| 高流量的 JSON API | 只在回傳 JSON 時啟用 compression,使用條件式 middleware 減少不必要的壓縮。 |
| 上傳檔案服務 | 僅在 POST /upload 路由內載入 multer,其他路由不會初始化檔案緩衝區。 |
| 多租戶系統 | 根據 Host 或 X-Tenant-ID 決定是否載入租戶專屬的驗證 middleware,避免所有租戶共用同一套檢查。 |
總結
在 Express + TypeScript 專案裡,避免不必要的 middleware 啟動 是提升效能的關鍵一步。透過:
- 路由層級的精準掛載、
- 條件式或懶載入 的設計、
- 環境變數切換 及 效能監控,
我們可以在不犧牲程式可讀性與維護性的前提下,顯著降低每次請求的 CPU、記憶體與 I/O 開銷。從今天開始,檢視你的 app.use 清單,將不必要的 middleware 移到更適合的層級,讓你的 Express 服務跑得更快、更穩。祝開發順利!