這篇文章可能不適合所有人閱讀,要理解它在說什麼,你至少得是一個準前端開發者或全端開發者,對 SSR、SSG、ISR 這些概念有基本的認知,且至少用過 Next.js
前言
可能是我天生對底層原理有非常強的好奇心,加上有時候執著於問清楚為什麼不能這樣做;我猜大概是這樣,所以有時候我才能想到一些獨特的想法:這就是其中之一,早在 25 年 11 月它就冒出來了,當時跟身邊很多朋友聊了聊,但因手頭項目時間衝突,一直拖到 2026 年新年前後才真正動手做出一個版本。之後又擱置了——因為我去建站了。雖然部落格還沒完全搭好,但總算有一個還算能記錄東西的地方,所以我想把這件事重新拎出來說一說。
首先先聲明一點:我不是在說 SSR 不好。我只是覺得,從我看到的趨勢來說,太多人在濫用它,大方向在隨波逐流地逃避一個問題。而我也跟不少人討論過這個,得到的回應大多不太確定;我也說過我的構想,但對方總是擔心這裡面會存在各種問題、各種邊界,而且說實話我也不太想每次都把整套思路從頭到尾完整講一遍。所以今天索性把它詳細寫下來,試著把一些疑問和技術原理講清楚吧。
被濫用的 SSR
早在刚入門 Web 前端開發的時候,第一個接觸到的全端框架應該就是 Next.js。 我承認這是一個非常優秀的框架,儘管它有一些效能問題,但這些效能問題是實打實的 DX 體驗換來的,這一點無可厚非。但隨著我慢慢深入理解前端的一些概念和知識,我發現它可能越來越不適合我了——因為我很喜歡寫 Rust,也很喜歡研究底層,玩過嵌入式,所以對效能有一種很特別的執著;執著也不是什麼都要極限,而是,這裡本來就有效能明顯更好,但是努力也許很小的辦法可以實現,為什麼大家都不這樣做?所以每當用起 Next 的時候我就會想:為什麼哪怕一個很簡單的頁面,大多數東西都是固定的,只有一個地方需要變,這整個頁面就要重新渲染一遍?為什麼?在我的理解中,哪怕後端不是用 Rust 寫的,哪怕不追求極限,就算跑在 JavaScript 直譯器上,真正改變的那一點資料也應該是一個相對來說很小的開銷,用一種很低成本的方式實現「渲染」,而不是重新渲染整個頁面。
以至於到了後來,它變成了我心裡的一道坎,成了我不得不思考的問題。但諷刺的是,當我看完前端這近 10 年的發展,Vue、Svelte、Solid 和他們的框架 Next.js、TanStack Start、Remix、Nuxt、Sveltekit 和 Soild Start 之後,我覺得似乎大家都走偏了,尤其是 React 的 RSC 是一個錯誤,但它的存在其實也是有理由的,這點以後有機會也許再拿出來詳細說說...
在我看來,RSC 和一眾 SSR 框架其實都在濫用 render 這件事。SSG 倒是做到了我想要的風格——頁面在建置時就生成好了,請求時不需要重新渲染——但它沒有真正的動態能力(x)。而 ISR 呢,看起來是在 SSG 和 SSR 之間找了個平衡,做到了一定的靈活性,但它本質上並沒有達到我的期望;我覺得 ISR 的出現其實就是為了彌補 SSR 效能差的問題,包括後續的各種 CDN 快取策略之類的操作,它們其實都算是一種 workaround,而不是真正優雅的解決方案。
當然 SSR 本身不是一個錯誤,它是一個很好的東西,解決了很多真實的場景。但問題在於,這些框架把 SSR 推到了一個極度預設選項的位置上,導致開發者真正用到 SSR 的時候,可能 95% 的場景其實根本不需要它。頁面上大部分內容都是固定的,真正動態的部分少之又少,但每次請求還是要把整棵元件樹跑一遍。
不過呢近幾年 React 倒是有所改觀,React 19.2 有一個 PPR,但這東西其實在執行時期還有昂貴的 renderToString(),也算是一個 workaround 而不是真正的解法。所以我就想,既然大部分東西在建置時就已經確定了,那為什麼不把渲染這件事直接放到編譯期去做呢?
編譯期渲染?
把渲染放到編譯期聽起來是一件很荒謬的事情,因為 SSR 存在的核心理由就是有些值、有些條件,不到執行期那個點上你根本解不出來,你不知道它是什麼,做不了判斷。所以我想大多數人第一時間否決我的想法也不是沒有道理,可以理解。
但其實我想到了算得上精巧的 Pipeline 來實現這件事,這也是為什麼我需要把它叫做一種 Protocol 另外就是我一直覺得全端框架混淆前後端邊界是一件很蠢的事情(雖然不得不承認近幾年 Next.js 在這方面 DX 做得很好,這讓很多初學者覺得寫一個全端的應用是一件很簡單的事情,但是實際上很多安全隱患也是由此而來的,扯遠了)
所以在組件裡寫資料獲取也是一樣,我更傾向於一種明確界限的方式——組件就是純組件,資料獲取應該被徹底抽出來。而一旦你接受了這個前提,純組件和資料分離之後,事情就變得有意思了:你會發現資料本身其實是可以被歸類的。
這裡也要感謝 TypeScript 給我的靈感,其實所謂的 "值" 本身在編譯期是什麼並不重要,重要的是它的型別;一個 slot 裡要填什麼內容,只要它不是那種 Open String (有無限種可能的字串),它就可以被包裹為有限可能性的型別。比如你在寫一個 dashboard,需要條件渲染某塊內容,無非就是 User 或 Admin 等等但是總歸是可以定義完的型別;真實世界中幾乎所有需要條件渲染、需要做邏輯判斷的地方,都可以被歸納為幾種確定的可能性。
那正好這些可能性可以用一個很好的辦法來描述,那就是 JTD!
JTD 規範
在 JTD (JSON Type Definition) RFC 8927 的定義中,它有八種 Schema Form:Empty、Ref、Type(boolean、string、timestamp 以及各種精度的數值型別)、Enum、Elements、Properties、Values、Discriminator,再加上任意 schema 都可以標記為 nullable。JTD 的好處在於它是跨語言的,JavaScript、Rust、Go 幾乎所有語言都能找到對應的型別對應,這就讓它天然地成為了前後端之間的橋樑,並且載體是 JSON,本來就是前後端最小公約。真實世界中那些網站 95% 的應用,無非就是顯示字串、數值欄位、或者用布林值做判斷,它們全都落在這個範圍內。明確了這一點後,實際上我們可以操作的空間就很大了,當然 JTD 也不是完美的,其實也有例外,比如 Markdown 這類東西,但這個我們後面再詳細說,這裡其實才是我真正認可 SSR 有意義的地方。
Sentinel
先說好辦的,既然我們已經知道每一個動態值都有型別,那我們就可以在編譯期做一件事:把 React 元件用 renderToString() 跑一遍,但不是用真實資料,而是 Mock 出來用 Sentinel。那麼這是什麼意思呢?
{ user: { name: "Alice", age: 30 } }假設你的資料長這樣,那麼我們可以把它替換成
{ user: { name: "%%SEAM:user.name%%", age: "%%SEAM:user.age%%" } }這些 %%SEAM:...%% 就是 Sentinel,它們佔住了每一個動態值的位置。React 拿著這些 Sentinel 資料跑完 renderToString() 之後,輸出的 HTML 裡這些位置就被標記出來了。接下來建置管線會把這些哨兵轉換成 HTML 註釋形式的 slot 標記。
<!-- Sentinel -->
<span>%%SEAM:user.name%%</span>
<!-- Slot -->
<span><!--seam:user.name--></span>到這一步你可能已經看出來了這些 slot 就是 「有類型標記的插槽」。這個時候伺服端執行時要做的事情變得極其簡單,那就拿到真實資料,往這些孔裡填值,純粹的字串替換。完全不需要 renderToString(),不需要 JavaScript 執行時,不需要 vDOM。任何能解析 HTML 註解、能做字串替換的語言都能當後端,Rust、Go、TypeScript 都行,這就是為什麼它是一個協定而不是一個框架。
這個時候你可能會想到條件渲染該怎麼辦?例如某個欄位為 null 的時候一整塊內容都不應該出現,但是其實這個也不需要運行時的 JavaScript 來判斷,相反地,協定可以定義這種情況。
<!--seam:if:user.avatar-->
<img src="<!--seam:user.avatar-->" />
<!--seam:endif:user.avatar-->條件渲染如此,列表渲染也一樣
<!--seam:each:messages-->
<li><!--seam:$.text--></li>
<!--seam:endeach-->甚至模式比對
<!--seam:match:status-->
<!--seam:when:active--><span class="green">Active</span>
<!--seam:when:disabled--><span class="red">Disabled</span>
<!--seam:endmatch-->這就是註釋的藝術,這裡選擇註釋是因為它正好是合法的 HTML 字元,僅此而已。那這些條件塊和迴圈塊是怎麼在 build-time 確定並產生的呢?其實也是一個算得上很巧的辦法,我們雖然沒有 vDOM 但是可以兩次渲染 HTML 做 Diff;拿條件渲染來說,第一次用完整的資料 Sentinel 渲染,第二次把某個 nullable 欄位設為 null 再渲染一遍,兩次輸出比對之下,消失的那塊 HTML 就是這個欄位控制的條件塊,直接用 <!--seam:if:...--> 包起來就好了。陣列也是同理。
也許 CTR?
那肯定又有人要擔心了:條件渲染的組合不會指數爆炸嗎?比如 3~5 個變數控制條件渲染,每個變數有 10 種可能,乘起來確實是一個看起來很大的數字。但實際上這個數字只是看起來嚇人而已嘛。首先,很多字串類型的值我們並不需要真的窮舉內容,我們只關心它有值還是沒值 nullable 這裡實際上就是 2 種;其次,真正需要做條件判斷的欄位,它的類型一定是可窮舉的,就比如(你不會拿一個 open string 來做 if 判斷吧?);再說了,就算真的要窮舉所有組合,在現代 CPU 上跑完可能也就幾 ms,而且這完全是編譯期的重,就像 Rust 編譯一樣,編譯期付出比較重的代價,但運行時就變成了純粹的字串替換,這筆生意怎麼算都是劃算的吧,更何況這裡沒有任何特別的魔法。但是這裡也可以埋一個小點:後面我其實引入了一個很小的嵌入式 JavaScript 執行環境來做一些複雜的推導,但這算是一種妥協,後面會解釋原因,以及理論上是可以不要的。
這就是我說的 CTR (Compile-Time Rendering)。至於限制,其實和 SSR 的限制是一模一樣的,比如 <!--seam:path--> 做文字插入時會自動做 HTML 轉義(&、<、> 等),如果你需要插入原始 HTML 就得顯式用 <!--seam:path:html-->;缺失的資料路徑在文字 slot 中會變成空字串,在屬性 slot 中會跳過注入;each 塊如果拿到的不是陣列就直接跳過。這些行為和你在 SSR 框架中遇到的邊界情況本質上沒有區別,我只是把 SSR 中的渲染這一步搬到了編譯期去執行,僅此而已。而 CTR 在 build-time 對所有條件變數的型別值做笛卡兒積走訪,使用 Mock 出來的任意符合型別的值一種 or 使用者 Override 特殊 Mock 值(一般是不需要手動寫生成 HTML 變體所需要的 Mock 值的,但是可以手動給,不然就是自動給一個符合你這個型別的值進來),這樣每種組合渲染一次,diff 出所有條件塊和迴圈塊的邊界,最終得到一個完整展開的 HTML 骨架。從數學上說,只要 runtime 傳入的資料符合這些型別定義,它就一定能被正確注入到這個骨架裡,所有可能的分支路徑在編譯期已經被窮舉過了。
一致性?
那一致性該如何保證呢,其實答案就是 JTD 這個 Contract, 前後端雖然分離了但只要都遵守同一份 JTD schema,資料的型態就是對齊的,不存在不一致的問題。
但我比其他框架多了一個挑戰,既然後端解放了 JavaScript 執行階段,它就不必和前端是一體的了,你完全可以用 Rust 或者 Go 來寫後端。那在 TypeScript 的全端框架裡,前後端一致性可以直接靠型態來保證;其他語言呢?實際上這裡我用了 codegen 無論後端用什麼語言寫,都以後端作為基準,把前端可以用到的變數和型態直接 codegen 成 TypeScript 給前端匯入。這樣雖然前後端的界線被劃清楚了,但它們實際上可以像 TypeScript 的全端框架一樣活在一個資料夾裡,符合 Monorepo 習慣,可以直接互相呼叫,不用手寫 API 或者 gen OpenAPI 之類的東西,實際上這裡我用了一個私有路徑 /_seam/ 作為框架預設端點(和 Nuxt 一樣可以設定的),這樣跑了 JTD Typed-RPC 就解決了資料傳輸和 CORS 的問題。
CTR x SSR
然後就是 raw HTML slot ,這個其實就是回到了那 5% 的 Edge Case,比方說 Markdown、Rich Text 這類東西,編譯期根本不可能知道它的值,而且要用型別去約束它的代價非常大。當然你可以說,把協定拓展一下,把 Markdown 的所有語法都列舉出來,執行期再做解析,但那個工作量基本等於你重寫了一個 Markdown 渲染器?而我們只約束最小的那幾種型別,和重寫整個渲染引擎的工作量完全不是一回事,這也回到了我最開始說的哲學,用更小的代價、更優雅的方式來解決問題。
所以好消息就是 raw HTML slot 其實可以讓 CTR 和 SSR 共存,它意味著你一個頁面大部分 UI 都用 CTR 來做,幾乎沒有執行期成本;然後最核心的那一篇 Markdown 文章,你用 SSR 來渲染。由於後端已經解放了,你可以選擇用 TypeScript 匯入你原來那套 SSR 的渲染方法,也可以思路打開!後端用 Rust 那當然是用 Rust 的 Markdown 編譯器來渲染,反正最後只要給出一個 HTML 字串,插入這個 raw slot,它就能顯示。用了 CTR 不意味著不能用 SSR,兩者完全可以共存。
PPR 的理想與現實
這樣理解下來,CTR 本質上就是 PPR(Partial Prerendering) 最理想的情況,所有能快取的東西全部在編譯期渲染完了,零執行期開銷,只有真正變化的那一點資料才有成本。那和 React 19.2 的 PPR 不同的是什麼呢?答案是我更徹底,更激進 PPR 就算在最理想的情況下,所有靜態部分都快取好了,只有一小段動態內容需要更新,但哪怕這個動態內容只是一個簡單的字串變了,React 還是得重新跑一遍 renderToReadableStream(),這個代價是不小的。
而 CTR 把這個界限劃得很清楚:如果是簡單型別比如字串、數字和布林值,那都是直接做字串替換;只有 Markdown 這種窮舉代價極大的複雜型別,才會走真正的 render 流程。而且這裡的 render 已經不是傳統意義上 SSR 的概念了,它可以是任何程式語言的渲染方式。
RSC 也不能少
那再說說 RSC(React Server Component)。之前說過 RSC 可能是一個錯誤,那是因為它模糊了太多前後端的邊界,帶來了不少安全性隱患 和 眾多 CVE,但其實這更多是錯在 Next.js 和其他的 SSR 潮流,使用者根本沒有機會不用,變成了你要動態就要 SSR,但是實際上我們這樣子的字串替換也是完全走得通的。另外不得不承認的就是 RSC 賦予了服務端一個很重要的能力,那就是渲染任意 React 元件,也就是執行任意程式碼。
不過你都要執行任意程式碼了,想用型別把它窮舉出來基本不可能,就算可能那工作量也不見得比寫一個 React 編譯器小。所以 RSC 這種能力是有它存在的理由的。CTR 和 RSC 能不能共存?答案是可以的,並且完全不衝突,但這屬於框架層面要解決的事情,不是協定本身的範圍,但我暫時還沒實現,不過這個似乎可以去抄 TanStack Start,只要多發一個 html 一個 js 包,理論實現還是很容易的,至少都是清晰可見的工作量。
再說回 raw HTML slot 這個解決主要還是 Markdown、Rich Text 這類場景,渲染出來的 HTML 透過 dangerouslySetInnerHTML 注入,水合後這塊區域不參與互動,是 "死的";這部分內容應該被最小化,你想給文章加邊框、加樣式,那些應該用 React 元件來寫,而不是混在這段 HTML 裡。這種做法的缺點是它不能在水合之後發生改變,但它確實把這種前提下的 "SSR" 成本壓到了非常低,接近於 Zero Cost 了。在真正需要 SSR 的場景裡,大概 60% 都是這種靜態 HTML 注入的情況;剩下 40% 才需要 RSC 那種伺服器端執行任意元件的能力。
前端 UI 無關性
最後就是很誘人的協定無關性,我們本質上只需要抓住 renderToString 這一個關鍵點,前面用什麼 UI 框架其實和協定沒有關係。那這樣的話,和 Astro 有什麼區別呢?不過放心,我肯定造的不是另外一個 Astro。表面上看我和 Astro 的島嶼概念有一點像,但實際上區別很大。
我並沒有在單一頁面裡水合多個運行時,Humm,其實我覺得你真正需要這個的場景非常有限,不同技術棧之間的組件狀態通信會變得非常昂貴,它更像是一種過渡方案,從一個技術棧遷移到另一個,一次性換不完所以先過渡。其次,Astro 本質上是 MPA,而我們可以做到水合前是 MPA、水合後變成 SPA,就像 Next.js 一樣有客戶端路由,可以做跨頁面動畫,這是 Astro 求之不得的。
Astro 與 SSG
另外就是 Astro 的設計本來就是在跨多個技術棧的場景才真正有用,如果你用冷是為了速度快的話,只用它的一個框架,比如只引入 React 不要 Vue,那它所說的 "速度快" 我覺得是一個偽命題,首屏載入出來都是 HTML 沒錯,但你想要任何互動它就要水合,水合的成本就是下載整個 React Runtime,這和我們的水合沒有任何本質區別。當然,後續我們也可以走通 Island 的概念,添加一個 shell router 來實現跨 UI 框架的 SPA 導覽,但這是後面的 Roadmap,至少我現在不急。
最後就是傳統 SSG 的對比,我做到了和 SSG 一樣的事情,本質上就是把編譯期能確定的全部渲染好;但我們更加動態,因為那些簡單型別的 slot 值是完全可以在執行期替換的。你可以理解為,我們把 SSG 渲染成了一種 MPA 的入口,水合前是 MPA,水合後變成 SPA,同時保留了 "動態" 能力。
也許告別水合錯誤?
最後就是我很討厭的 hydration mismatch,我相信你也不喜歡;但是歸根結底這其實就是 DOM 狀態不一致,而傳統框架比如 Next.js 的 App Router 試圖把整個應用都用 React 包裹起來,一旦使用者端有瀏覽器注入什麼標記,就有機率出現水合錯誤。我們和傳統 SSR 的限制其實是一樣的,但是我在你用 TS 的時候也包裹了一個 叫做 __root 的水合 div 這樣讓水合區域不再覆蓋 metadata 區域,換來了更加耐用的水合,另外 React 19 內建了對 <title>、<meta>、<link> 等文件元資料標籤的支援。即使你水合的只是頁面中的某個 <div>(而不是整個 <html>),在元件裡直接渲染 <title>My Page</title>,React 也會自動把它提升 hoist 到 <head> 中去。
回到 hydration mismatch,已知我們在編譯期已經預知了每個 slot 的型態,所以可以多加一道 CTR 等價性檢查,那就是用基於型態定義推導出來的 mock 資料填入完全展開的 HTML,再呼叫傳統的 renderToReadableStream() 跑一遍,然後比對兩者的 DOM-Tree 語意是否等價,不關心格式 fmt 差異,它們可能有格式上的細微差距,但只要 DOM 結構上嚴格完全等價其實是可以做到告別 hydration mismatch 的;為什麼傳統 SSR 做不到?因為它們是在執行期才做這件事,而 CTR 結構決定了你在編譯期就要把這些約束全部落實到位。我們當然也有 any 的逃生入口,但是至少和 TypeScript 一樣,你用了 any 就要主動承擔後果,CLI 編譯的時候會 warn 你,any 逃生 open string 可能會導致 mismatch 的。
探索 Serverless
當然還有 Serverless 的可能性也不能少,最近幾年 Serverless 的體驗可以說是非常好了,我評價為「除了費錢沒啥缺點了」,然而 CTR 天然非常適合這個場景,我們執行時期做的事情很輕很小,Serverless 跑起來會非常快,回應時間和開銷都會得到明顯提升。剩下提升不了也就是渲染 Markdown 那種場景,但那完全應該在商業邏輯上做優化比方說把 Markdown 預渲染好存起來,每次請求不用重新渲染,就像我現在這個網站做的一樣。這是商業層面的問題,不是框架能解決的,但框架能幫你把傳統 SSR 除此之外的那些簡單邏輯的開銷壓到幾乎為零。和傳統 SSR 對比,它不再是一個數量級的開銷,有多小呢,大概就是幾百 us ~ 1ms 之間的事情了,這是傳統 SSR 想都不敢想的。
那如果後端是其他語言怎麼辦?就拿 Cloudflare Workers 舉例,其實很多 Serverless 都支援 WASM,其他語言編譯成 WASM BIN 之後一樣可以作為後端。本質上我就是把前端編譯成純靜態資源,類似 CSR 一樣,但這堆純靜態資源配合一個私有橋 /_seam/,就能實現和那些真正的全端框架一樣的動態能力,自然也完全相容 Serverless 了。
SeamJS
所以你是不是已經被 Seam 和 SeamJS 的關係繞暈了?其實這兩者很簡單:Seam 是協定,它定義的是怎麼用 Sentinel 標記動態位置、怎麼轉換成 slot 標記、怎麼做條件塊和迴圈塊的 diff 檢測、以及執行期怎麼基於 AST 做資料注入。協定本身是語言無關的,任何能解析 HTML 註釋、能做字串替換的後端都能實現它。SeamJS 是框架,是基於這套協定做的一個具體實現 它把 Vite、TanStack Router、TanStack Query 這些輪子縫在一起,然後補它們覆蓋不到的地方,比如我這套 skeleton 擷取、注入引擎、CLI 之類的。
但說實話,SeamJS 現在還是一個非常基礎的樣子。跑起來是沒什麼問題的,但真的拿它去寫專案的話還需要打磨很久,我也在更多的思考一些架構上的改革,比如資料傳輸通道的抽象,但我並不確定在後續版本中還會不會堅持這個方向?框架層面上,我肯定會做出一個 TypeScript 的全端框架,以及一個 Rust 作為後端的旗艦框架;至於 Go 相關的實作後續版本中移除掉,覺得自己的精力維護不過來了。
不只是 Web 框架
那這個東西做出來有什麼用呢?實際上它不只局限於 Web。比如說我把 Transport 通道也給抽象掉之後,是可以後續移植到類似於 Electron 或者 Tauri 之類的 Desktop 環境的,只要把 HTTP 傳輸管線換成 IPC 通信,跑的還是一樣的 Seam 協議,取而代之的就是那些 Electron 應用開屏的時候再也不用放 loading 了,很多東西能夠直接像 SSR 一樣本地渲染出來,這就是 CTR 的魔法!
No JS Runtime 的代價
那為什麼最後 SeamJS 還是加入了 JS Runtime 呢?因為在落實這套方案的時候我遇到了一個問題,那就是 CTR 的理想對首屏資料的約束是非常嚴格的,資料必須是一個可以完全推導出來的確定結構體,甚至不能有任何計算邏輯,只能是條件。這就變得非常苛刻了,可想而知的是作為框架的開發體驗來說,傳統寫 React 的人肯定是指望在元件裡做一些計算、拿到幾個值就能用,如果要嚴格按照 CTR 的約束來達到一個 ready-to-display 的狀態,過程會非常繁瑣,甚至出現一個元件寫 2 次。
但是其實有解,因為 Web 原因,前端只能跑在 JavaScript 上,所以理所當然元件只能跑 JS,所以後端就必須要有 JS 執行能力才能解決這個問題,TypeScript 全棧專案本身的運行時就能用來解決;而 Rust 裡面只需要嵌入一個 QuickJS 這樣非常微小的 JS Runtime 就好了,注意這裡的 JavaScript Runtime 是標準的 JS 子集,完全不是 Bun 或 Node 那種帶完整作業系統 API 的運行時,真的就只是用來做推導、做 derive。
這樣就可以在組件裡還是寫一些計算邏輯,這部分邏輯會在後端拿到數據之後跑一段小 JS,把它推導為 ready-to-display 的狀態,然後再發回給前端做首屏的水合。這樣 CTR 滿意了,拿到了嚴格結構的派生數據,開發者 DX 開心了,組件只寫一遍。當然如果你願意嚴格遵守純類型約束的話,後端確實可以做到 No JS Runtime,這個承諾其實還是成立的。但無論怎麼說,新增的這個很小的 JavaScript Runtime,它的性能開銷、內存佔用和體積都遠遠比 Node 之類的東西小得多,基本上就是 200-300KB 的事情,卻帶來了非常大的靈活性。
未來展望
說了那麼多,什麼時候能吃上呢:大概吧,還要很久很久,真的不是我想鴿子,而是其實我覺得現階段這個玩意綁架我太多太多了。我只能拿來開發的話,基本上應用寫不了,寫什麼都被框架先絆腳,更何況我最近主要其實都在寫這個網站什麼的了。所以我想通了,不如網站先寫上一個規模,這樣就知道我的需求到底是什麼;後面也許再來寫 SeamJS 就有一個 TODO List 了,一個一個實現好這個功能就能用了,後續還能再水一個 遷移 和 Benchmark ?好了,大餅畫完了,理念就在這,先別管今天能不能用,至少思路上原型已經跑通了,晚安 💤