為什麼要知道這些老東西

選框架的時候,最怕聽到的理由是「大家都用這個」。

這句話的問題不在懶,在於你不知道它當初是為了解決什麼而生的,就沒辦法判斷你的情況適不適用。Express 很輕,但你的團隊有 30 個人要一起改同一份程式碼,那個「輕」會變成每個人各寫各的。Spring Boot 很完整,但你只是要接一個 webhook 回個 200,那個「完整」就是純粹的負擔。

每一代框架都是某一代人撞牆撞出來的。知道牆在哪,你才知道自己現在站在牆的哪一邊。


先搞懂一件事:一千個人同時敲門,你怎麼開

整個 web framework 演進史,吵的其實只有這一件事。

把 server 想成一家餐廳,請求就是上門的客人。餐廳怎麼服務這一千個人,只有三種基本做法——後面每一代框架的選擇,都是在這三種裡面挑,或是想辦法繞過它們的缺點。

Process-based(每個請求配一個行程):每個請求分配一個 OS process(行程,作業系統給你的一塊獨立工作空間,彼此不共用記憶體)。

這就像一個客人就派一個專屬服務生,用自己的紙筆、自己的推車,完全不會跟別人搞混。代價是養一千個服務生會破產——process 建立成本高(fork 費時)、記憶體用量大(一個 process 幾十 MB)。

Thread-based(每個請求配一個執行緒):每個請求分配一個 thread(執行緒,同一塊工作空間裡可以同時做事的分身)。Thread 共享 process 的記憶體,建立比 process 快——像是服務生共用同一本訂單簿,省了紙錢,但麻煩跟著來:

  • Context switch 成本(切換分身時,作業系統要先把手上做到哪裡記下來、再翻出另一份,這個「換手」本身就要花時間)
  • 共享記憶體帶來競態條件(race condition,兩件事搶著改同一份資料而出錯)——兩個 thread 同時改同一個變數,需要鎖(mutex / lock,同一時間只准一個人動的門閂)保護,而鎖本身又是效能瓶頸。就像兩個服務生同時要在訂單簿的同一行寫字,只好排隊等對方寫完
  • Thread 等 I/O 時什麼都不做,但它佔的資源還在那裡——服務生站在廚房門口乾等出餐,薪水照付

Event Loop(async I/O,一個人記住所有進度輪流處理):單一 thread 管理所有請求。I/O 等待期間(讀資料庫、call 別的 API 這種「送出去等回來」的動作),thread 不阻塞,改去處理其他事情;I/O 完成後觸發 callback 繼續。

這是一個超強的服務生,點完 A 桌的單就馬上去接 B 桌,廚房出餐了再回頭送。不是真正的並行(同一瞬間仍然只有一個人在動),但 I/O 密集場景下吞吐量遠高於 thread-per-request。代價很明確:CPU 密集任務會卡住整個 event loop——這個服務生要是自己蹲下來剝一百顆蒜頭,全場的客人都得等他。

這三種模型對應了整個演進史的選擇邏輯。往下看每一代撞的牆,其實都是在這三者之間換位置。


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

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

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

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

撞牆點:請求量超過幾十 concurrent 就開始扛不住。process 的記憶體 overhead 太高(每個 process 幾十 MB),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)解的不是效能問題,解的是可維護性問題——PHP 時代那種「能跑就好,一年後沒人看得懂」的爛攤子。

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

  • MVC 強制分層:把程式碼拆成三種角色——Model(管資料)、View(管畫面)、Controller(管「這個請求該找誰處理」)。各有各的資料夾,你不能把業務邏輯寫進 View
  • Convention over Configuration(慣例優先於設定):照著約定好的命名走,就不用寫設定檔。你的 User model 自動對應 users table,不需要任何設定
  • ActiveRecord:ORM(物件關聯對映,讓你用操作物件的方式去存取資料庫)直接讓你用物件操作資料庫,SQL 不用手寫
  • Generator:rails 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,全域直譯器鎖——同一時間只准一個 thread 真的在執行)。開了十個分身,實際上仍然只有一個在動,無法真正並行。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 等待期間可以處理其他請求
→ CPU-bound 任務(大量計算)仍會卡住 event loop,解法是 Worker Threads 或 background queue

這解釋了為什麼在 Node.js 生態裡,Queue(Bull / BullMQ)這麼重要——CPU 密集或耗時的任務(圖片處理、PDF 生成、大批次匯出)不能放在 event loop 的 request handler 裡跑,要丟到 queue 讓 worker process 另外處理,主 event loop 才不會被卡住。

Express(2010)是 Node.js 生態的第一個主流 framework,極度 minimal:只給你路由和 middleware(中介層,請求在進到你的處理程式之前會先經過的一串關卡,例如驗身分、記 log),其他都自己來。這個設計哲學讓它在 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(Python 世界訂出來的「web server 怎麼把請求交給你的程式」的共同介面,而且是一次處理一個、處理完才換下一個)。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。


Microservice 架構崛起:部署單位的革命

2013–2018 年,另一條平行的演進線改變了整個後端的架構方式——把一個大 monolith(單體,整個系統打包成一份程式、一起部署)拆成很多小 service。

Monolith 的撞牆點不是效能,是組織規模。程式碼沒有變慢,是人變多了:

  • 部署耦合:一個 bug 修完要把整個 app 重新部署,100 人的工程團隊同時改同一個 codebase,deploy 頻率卻受最慢的那個人限制
  • 故障擴散:使用者系統壞了,整個電商後台也跟著壞——因為在同一個 process 裡
  • 技術鎖定:你選了 Java,整個系統就是 Java。想用 Python 做 ML pipeline、用 Go 做高並發服務?在 monolith 裡很難

Netflix(2008–2012)、Amazon(2002 年 Bezos 的 mandate)把這個方向推到工業級規模。每個 service 獨立部署、獨立 scale、甚至可以用不同語言寫。

Microservice 對 framework 選擇的影響:一個 microservice 可能只做「處理 payment webhook」或「發送 email notification」這一件事——這個規模的 service 根本不需要 Django 或 Spring Boot 這麼重的框架。Minimal framework(Express / Gin / FastAPI)剛好符合「輕量、只做一件事」的 microservice 定位。

Microservice 帶來的新問題:service 多了,問題也變複雜:

  • 網路呼叫取代 function call,latency 和 failure 需要處理
  • 分散式資料一致性(跨 service 的 transaction 很難做)
  • 服務發現(service 們搬來搬去,怎麼知道對方現在在哪個位址)、load balancing(流量分給多台機器)、circuit breaker(斷路器,下游掛了就先停止呼叫,免得自己被拖死)
  • 分散式 tracing(一個請求可能跨 5 個 service,出錯怎麼追)

這些問題催生了 Service Mesh(Istio)、API Gateway、分散式追蹤(OpenTelemetry)的需求——但那已經是 infra 層的故事了。


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 為什麼還活著 有完整的分析。


延伸閱讀