本文 AI 產出,尚未審核

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.allrouter.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 大型模組 會重複載入,降低效能。 只在需要時懶載入,或在程式啟動時一次載入。

最佳實踐總結

  1. 先思考「誰需要」,再決定 middleware 的範圍。
  2. 使用 TypeScript 的型別,在自訂 wrapper 中明確標註 req, res,避免因型別不合而誤用。
  3. 懶載入(lazy‑load) 大型外部套件,尤其是只在少數路由使用的功能。
  4. 環境變數切換:開發、測試、正式環境的 middleware 配置應分離。
  5. 效能監控:使用 response-timeprom-client 等工具量測每個 middleware 的耗時,找出瓶頸。

實際應用場景

場景 建議做法
健康檢查 endpoint (/health) 不掛載任何認證或日誌 middleware,直接回傳 200 OK,確保監控系統的請求最快。
大量公開靜態資源 (/public/*) 使用 express.static排除所有全域 middleware,只保留必要的 CORS、Cache 控制。
高流量的 JSON API 只在回傳 JSON 時啟用 compression,使用條件式 middleware 減少不必要的壓縮。
上傳檔案服務 僅在 POST /upload 路由內載入 multer,其他路由不會初始化檔案緩衝區。
多租戶系統 根據 HostX-Tenant-ID 決定是否載入租戶專屬的驗證 middleware,避免所有租戶共用同一套檢查。

總結

在 Express + TypeScript 專案裡,避免不必要的 middleware 啟動 是提升效能的關鍵一步。透過:

  • 路由層級的精準掛載
  • 條件式或懶載入 的設計、
  • 環境變數切換效能監控

我們可以在不犧牲程式可讀性與維護性的前提下,顯著降低每次請求的 CPU、記憶體與 I/O 開銷。從今天開始,檢視你的 app.use 清單,將不必要的 middleware 移到更適合的層級,讓你的 Express 服務跑得更快、更穩。祝開發順利!