HTML-first 還是 SPA:把預設調回來,不是打聖戰
HTML-first 還是 SPA:把預設調回來,不是打聖戰
每次有人問「這個專案要不要上 SPA」,我都想先反問一句:你這畫面的狀態,到底主要活在 server 還是 client?多數人答不出來,因為從來沒想過這件事,反射性就 create-vue、create-react-app 開下去了。預設錯位才是真正的問題,不是 SPA 本身有罪。
htmx 2.0 在 2024 年中發布之後,server-first 這條路又被重新拿出來討論。它沒帶來什麼新魔法,核心從第一天就沒變:送 HTML 片段、狀態留 server、瀏覽器局部換 DOM。htmx 真正的價值,是逼你問一個一直被跳過的問題:這塊互動,真的需要把狀態搬到前端嗎?
狀態活在哪,答案就在哪
我給自己的判準很簡單,就兩個問題。第一,這個畫面的狀態主要活在 server 還是 client?第二,使用者的互動是「換掉一塊內容」還是「即時操作大量本地狀態」?
CRUD、表單、workflow-heavy 的後台,狀態幾乎都活在 server。使用者按一下、送一個 request、後端算完回一段 HTML,畫面換掉。這種場景你硬要套 SPA,等於自己幫自己加了一層 JSON API、一個 state store、前後端兩套 model 要同步,還要處理 serialise/deserialise。問題是這些複雜度換來了什麼?換來一個本來用 form submit 就能解決的「新增一筆資料」。這就是典型的 over-engineering,把簡單的事情包成複雜的事情。
server-first 在這裡的好處不是「比較潮」,是責任邊界清楚。狀態只有一份,活在 server,前端只是它的投影。你不用煩惱兩邊不一致,不用煩惱 client 端 cache 過期,不用為了一個下拉選單寫 reducer。htmx、Hotwire 這類東西的價值就在這,它讓你保留 HTTP 跟 DOM 的直覺,不用先學一套框架的世界觀才能改一個按鈕。
真的需要 client 狀態的那些場景
但話講回來,我不是要否定 SPA。有一類東西,狀態天生就活在 client,你把它硬塞回 server 才是災難。
高互動的編輯器、canvas 繪圖、拖拉排版這種,每一個滑鼠移動都是狀態變化,你不可能每動一下就發 request。離線優先的應用更不用說,使用者根本沒有網路,狀態只能先活在本地。還有現在很多 local-first 的 SaaS,資料寫進 IndexedDB,記憶體裡維護一份狀態,CSR 都能做到接近原生的流暢度,這種你用 server-first 是自找麻煩。
判準還是回到那兩個問題。如果互動是「即時操作大量本地狀態」,狀態主要活在 client,那 client 狀態管理就是你該付的成本,付得心甘情願。這時候用 Svelte、用 React,搭一套 runes 或 signals 來管反應式狀態,完全合理。重點是你選它是因為畫面的本質需要,不是因為手滑開了 create 指令。
我自己踩過的坑很實在。有個專案一開始用 SvelteKit SSR,跑起來在某些環境就是慢,最後改成純 SPA 反而流暢,因為那個畫面互動密集、狀態原本就該在前端。反過來也有,後台管理介面我堅持用 server routes 當 SSR + BFF,外部請求不直接碰資料層,省下的前端複雜度多到不像話。同一個人、同一年,兩個相反的決定,差別只在那兩個問題的答案不同。
把預設調回 HTML-first
我想講的從頭到尾就一件事:預設值。反射性開整套 SPA 是預設錯位,你跳過了判斷這一步。把預設調回 HTML-first,意思是你一開始假設狀態活在 server、互動是換內容,先用輕量一層的方式做,真的遇到需要重本地狀態的畫面,再升級到 client。
這不是二選一的聖戰,是一個專案裡兩種模式並存。後台 CRUD 走 htmx,那個高互動的編輯器走 SPA,沒有矛盾。矛盾的是你不分青紅皂白,全部塞進同一套重型框架,然後在每一個簡單表單上付 SPA 的稅。預設先選便宜的那條,需要才升級,這樣你的複雜度才會花在刀口上。
重點整理
- 先問兩個問題:狀態主要活在 server 還是 client,互動是換內容還是操作大量本地狀態。
- CRUD、表單、workflow-heavy 後台狀態活在 server,server-first 讓責任邊界清楚、只維護一份狀態。
- 高互動、離線優先、canvas、local-first 這類狀態天生活在 client,別硬塞 htmx。
- 反射性開整套 SPA 是預設錯位,跳過了判斷這一步;把預設調回 HTML-first,需要才升級。
- 同一個專案可以兩種模式並存,複雜度該花在真正需要的畫面上。
觀點 Sheng,內文 Claude 協助 · 列入 20260613 blog 翻新計劃,新漆未乾。