CGI 時代:每個請求都是一個新 process

最早的 web server 處理動態內容的方式是 CGI(Common Gateway Interface,1993):

瀏覽器發請求 → Apache → fork 一個新 process 跑腳本 → 輸出 HTML → process 結束

每個請求都 fork 一個新 process。請求量一大,server 就被 fork 的成本拖垮。Perl 和 C 是 CGI 時代的主角,因為它們快。

撞牆點:請求量超過幾十 concurrent 就開始扛不住。process 的記憶體 overhead 太高,fork 的成本太大。


Apache Module 時代:把語言嵌進 server

解法是把語言直接嵌進 web server,不用每次 fork:

  • mod_php(1994):PHP 直接跑在 Apache 的 process 裡,不需要 fork
  • mod_perl(1996):Perl 也嵌進去

PHP 在這個時代爆炸性成長,因為它讓「在 HTML 裡面直接嵌程式碼」變得容易:

<html>
  <body>
    <?php echo "Hello, " . $name; ?>
  </body>
</html>

撞牆點一:效能好了,但程式碼品質爛了。 PHP 讓每個人都可以寫 web,但也讓每個人以不同的方式寫。沒有 MVC、沒有 ORM、資料庫查詢和 HTML 混在一起。專案超過幾千行之後,沒有人知道整個系統怎麼跑的。

撞牆點二:session 管理、路由、安全性每個人都自己實作。 結果是大量的重複和安全漏洞。


Rails 時代:Convention over Configuration

Rails(2004)解的不是效能問題,解的是可維護性問題

David Heinemeier Hansson 從 Ruby 的動態語言特性出發,做了幾件在當時很激進的事:

  • MVC 強制分層:Model / View / Controller 有各自的資料夾,你不能把業務邏輯寫進 View
  • Convention over Configuration:你的 User model 自動對應 users table,不需要任何設定
  • ActiveRecord:ORM 直接讓你用物件操作資料庫,SQL 不用手寫
  • Generatorrails generate scaffold User name:string email:string,CRUD 全部生出來

Rails 的影響遠超過 Ruby 生態。Django(Python,2005)、CakePHP(PHP,2005)、Spring MVC(Java)都在同一時期借鑒了這個思路。

撞牆點一:Rails 的 thread 模型。Ruby MRI 有 GIL(Global Interpreter Lock),無法真正並行。Rails 的 Puma / Unicorn 是 multi-process 或 multi-thread,但 context switch 成本和記憶體用量在高並發時成了瓶頸。

撞牆點二:Convention 也是負擔。Rails magic 讓初學者很快上手,但出了問題要 debug 就要讀大量的 DSL 和 meta-programming。而且專案越大,「你以為你改了 A,但其實 B 也動了」這種問題越多。

撞牆點三:JSON API 時代的 overhead。Rails 本來是為了 server-side rendering 設計的。JSON API 不需要 template engine,Rails 帶來的 view layer 整個是多餘的 overhead。


Node.js 時代:Event Loop 解高並發

2009 年,Ryan Dahl 做 Node.js 的出發點是:thread-per-request 模型在高並發下記憶體爆炸

一個 Apache + PHP 的 server,每個連線會占用一個 thread(或 process),10,000 個並發連線就是 10,000 個 thread——記憶體和 context switch 成本讓 server 撐不住。

Node.js 的 event loop 模型:

單一 thread + non-blocking I/O
→ 一個 process 可以同時「等待」數萬個 I/O 操作
→ 不是真正的並行,但 I/O 等待期間可以處理其他請求

Express(2010)是 Node.js 生態的第一個主流 framework,極度 minimal:只給你路由和 middleware,其他都自己來。這個設計哲學讓它在 microservice 時代大量被用在「只需要做一件事的 service」。

撞牆點一:Callback hell。早期 Node.js 大量用 callback 處理 async,巢狀 callback 讓程式碼難以閱讀和維護。Promise(2015 ES6)和 async/await(2017 ES2017)緩解了這個問題,但語言本身的 async 模型仍有學習曲線。

撞牆點二:Express 什麼都不給,大專案要自己搭所有東西。每個 Express 專案的結構都長得不一樣,onboarding 成本高。NestJS(2017)用 Angular 的架構理念解這個問題——強制 Module / Injectable / Controller,讓 Node.js 大型後端也有統一結構。

撞牆點三:動態型別的大型系統維護困難。TypeScript 的廣泛採用(2018 後)是為了解決這個問題。現在幾乎所有新的 Node.js 後端都用 TypeScript。


async framework 回潮:ASGI、Ktor、Spring WebFlux

Python 的 Django 是同步框架,底層是 WSGI。2018 年之後,async Python 框架開始崛起:

  • FastAPI(2018):基於 Starlette(ASGI),native async/await,Pydantic 自動 validation,OpenAPI 文件自動生成
  • Starlette(2018):FastAPI 的底層,輕量 ASGI framework

FastAPI 解的是幾個 Django 的痛點:

  • Django ORM 是同步的,在高並發 I/O 場景會因為 thread 阻塞拖慢整體吞吐
  • Django REST Framework 的 serializer 寫法太繁瑣
  • API 文件要手寫或用插件,不是自動的

JVM 生態也有類似演化:Spring WebFlux(2017)是 Spring 的 reactive 版本,解決 Spring MVC 的 thread-per-request 在高並發下的問題;Kotlin 語言帶來 coroutine,Ktor 以此為基礎成為 Kotlin 原生的 async framework。


Minimal API 的回頭浪

2022 年之後有一個有趣的現象:輕量 framework 再次流行。

  • Hono(2022,Node.js/Bun/Cloudflare Workers):比 Express 更快、typed route、跨 runtime
  • Elysia(2023,Bun):利用 Bun 的效能,端對端型別安全
  • ASP.NET Minimal API(.NET 6,2021):微軟把 .NET 的 API 寫法簡化,不用 Controller class

這波浪的驅動力是:NestJS / Spring Boot 這類 opinionated 框架,對小型 API service 來說太重了。如果你的 service 就是「接一個請求、查一次資料庫、回傳 JSON」,帶整個 Angular-inspired 架構進來是 overkill。

撞牆點(預測):minimal framework 在大型系統裡,還是會遇到「每個人結構不一樣」的問題。Meta-framework 和 convention-heavy 框架的下一波崛起,可能是因為 minimal framework 的 onboarding 問題又出現了。


演進的底層邏輯

每一代 framework 的出現都是因為:

  1. 前一代的解法在某個維度撞牆(效能、可維護性、開發速度、型別安全)
  2. 新的語言特性 / 語言生態創造了新的可能(GIL 的限制、async/await 的普及、TypeScript 的成熟)
  3. 部署模式改變(monolith → microservice → serverless → edge function)

這些技術不是「新的比較好」,而是「在不同的約束條件下,不同的 trade-off 更合理」。Express 在 2026 還活著,不是因為它沒有替代方案,而是因為它的 trade-off 在特定場景下仍然是最合理的——Express 為什麼還活著 有完整的分析。


延伸閱讀