在開始閱讀之前,先解釋一下文章里用到的英文縮寫:
- CSR:Client Side Rendering,客戶端(通常是瀏覽器)渲染
- SSR:Server Side Rendering,服務端渲染
- SSG:Static Site Generation,靜態網站生成
- ISR:Incremental Site Rendering,增量式的網站渲染
- DPR:Distributed Persistent Rendering,分佈式的持續渲染
從 SSR 到 SSG
SSR 這套技術棧相信很多人應該都非常熟悉了(如果你不熟悉的話可以先閱讀相關文章),React/Vue/Angular 等等都從框架層面直接提供了支持,例如在 React 中你可以這樣使用:
import React from 'react' import ReactDOMServer from 'react-dom/server' const html = ReactDOMServer.render(<h1>Hello, world!</h1>)
SSR 最早是為了解決單頁應用(SPA)產生的 SEO、首屏渲染時間等問題而誕生的,在服務端直接實時同構渲染用戶看到的頁面,能最大程度上提高用戶的體驗,流程類似下面:

但 SSR 引入了另一個問題,既然要做服務端渲染,就必然需要一個實時在線的後台服務(通常是基於 Node.js 的服務)用來承載頁面請求,那麼:
- 需要服務器的計算資源和公網流量來部署這套服務,並且消耗的資源與頁面的訪問量成正相關,當頁面的訪問量突增時,渲染服務也需要進行擴容;
- 服務端只能部署在有限的幾個地域,對於距離服務端較遠的用戶而言,加載速度跟靜態資源的 CDN 相比,慢了一個數量級(通常是 1-5ms VS 50-100+ms);
- 日常也存在傳統服務端同樣的運維、監控告警等方面的負擔,團隊需要額外的人力來開發和維護。
有沒有辦法解決這些問題呢?
我們重新對 SSR 進行審視,服務端渲染出的頁面,邏輯上講可以分成下面兩大塊:
- 變化不頻繁,甚至不會變化的內容:例如文章、排行榜、商品信息、推薦列表等等,這些數據非常適合緩存;
- 變化比較頻繁,或者千人千面的內容:例如用戶頭像、Timeline、登錄狀態、實時評論等。
例如,在一篇文章的頁面中,文章的主題內容是偏向於靜態的,很少有改動,那麼每次用戶的頁面請求,都通過服務端來渲染就變得非常不值得,因為每次服務端渲染出來大部分內容都是一樣的!
我們完全可以將文章的頁面渲染為靜態頁面,至於頁面內那些動態的內容(用戶頭像、評論框等),就通過 HTTP API 的形式進行瀏覽器端渲染(CSR):

這樣做有很多好處:
- 由於文章內容已經被靜態化了,所以它是 SEO 友好的,能被搜索引擎輕鬆爬取;
- 大大減輕了服務端渲染的資源負擔,不需要額外做一套 Node.js 服務;
- 用戶始終通過 CDN 加載頁面核心內容,CDN 的邊緣節點有緩存,速度極快;
- 通過 HTTP API + CSR,頁面內次要的動態內容也可以被很好地渲染;
- 數據有變化時,重新觸發一次網站的異步渲染,然後推送新的內容到 CDN 即可。
- 由於每次都是全站渲染,所以網站的版本可以很好的與 Git 的版本對應上,甚至可以做到原子化發佈和回滾。
這便是 Gatsby.js、Next.js 這樣的網站生成器解決的問題,他們屬於 React/Vue 更上一層的框架(Meta Framework),通過 SSR 把動態化的 Web 應用渲染為多個靜態頁面,並且對高度動態的內容也保留了 CSR 的能力。
從 SSG 到 ISR/DPR
細心的同學一定發現了 SSG 這樣的模式,看似美好,但存在一個瑕疵:
對於只有幾十個頁面的個人博客、小型文檔站而言,數據有變化時,跑一次全頁面渲染的消耗是可以接受的。
但對於百萬級、千萬級、億級頁面的大型網站而言,一旦有數據改動,要進行一次全部頁面的渲染,需要的時間可能是按小時甚至按天計的,這是不可接受的。
為了解決這個問題,各種框架和靜態網站託管平台都提供了不同的方案,這裡我們介紹 ISR 和 DPR 兩種。
ISR,Incremental Site Rendering
既然全量預渲染整個網站是不現實的,那麼我們可以做一個切分:
1、關鍵性的頁面(如網站首頁、熱點數據等)預渲染為靜態頁面,緩存至 CDN,保證最佳的訪問性能;
2、非關鍵性的頁面(如流量很少的老舊內容)先響應 fallback 內容,然後瀏覽器渲染(CSR)為實際數據;同時對頁面進行異步預渲染,之後緩存至 CDN,提升後續用戶訪問的性能。

頁面的更新遵循 stale-while-revalidate 的邏輯,即始終返回 CDN 的緩存數據(無論是否過期);如果數據已經過期,那麼觸發異步的預渲染,異步更新 CDN 的緩存。

這就是增量式更新(ISR)的概念,這個概念最早由 Next.js 在 9.5 版本中提出,下面是一個小 Demo:
在 Next.js 中,你可以使用 getStaticPaths() 來定義哪些路徑需要預渲染,通過 getStaticProps() 來獲取預渲染需要的數據:
// 定義哪些頁面需要預渲染 export async function getStaticPaths() { return { // 只有 /posts/1 和 /posts/2 會被預渲染 paths: [{ params: { id: '1' } }, { params: { id: '2' } }], // 其它頁面,如 /posts/3,都會返回 fallback 頁面,然後 CSR fallback: true, } } // 定義預渲染需要的數據 export async function getStaticProps({ params }) { // 拉取對應的文章內容 const res = await fetch(`https://.../posts/${params.id}`) const post = await res.json() return { props: { post }, revalidate: 60 // 數據有效期為 60 秒 } }
但 ISR 存在部分缺陷:
- 對於沒有預渲染的頁面,用戶首次訪問將會看到一個 fallback 頁面,此時服務端才開始渲染頁面,直到渲染完畢。這就導致用戶體驗上的不一致。
- 對於已經被預渲染的頁面,用戶直接從 CDN 加載,但這些頁面可能是已經過期的,甚至過期很久的,只有在用戶刷新一次,第二次訪問之後,才能看到新的數據。對於電商這樣的場景而言,是不可接受的(比如商品已經賣完了,但用戶看到的過期數據上顯示還有)。
具體關於 ISR 的利弊,可以進一步看 Netlify 的這篇文章:
Incremental Static Regeneration: Its Benefits and Its Flaws
DPR,Distributed Persistent Rendering
為了解決 ISR 的一系列問題,Netlify 在前段時間發起了一個新的提案:
Distributed Persistent Rendering (DPR)
DPR 本質上講,是對 ISR 的模型做了幾處改動,並且搭配上 CDN 的能力:
- 去除了 fallback 行為,而是直接用 On-demand Builder(按需構建器)來響應未經過預渲染的頁面,然後將結果緩存至 CDN;
- 數據頁面過期時,不再響應過期的緩存頁面,而是 CDN 回源到 Builder 上,渲染出最新的數據;
- 每次發佈新版本時,自動清除 CDN 的緩存數據。

在 Netlify 平台上,你可以像這樣定義一個 Builder,用於預渲染或者實時渲染。這個 Builder 將會以 Serverless 雲函數的方式在平台上運行:
const { builder } = require("@netlify/functions") async function handler(event, context) { return { statusCode: 200, headers: { "Content-Type": "text/html", }, body: ` Hello World `, }; } exports.handler = builder(handler);
更多詳細信息可以參考文檔:
當然 DPR 還在很初期的階段,就目前的討論來看,依然有一些問題:
- 新頁面的訪問可能會觸發 On-demand Builder 同步渲染,導致當次請求的響應時間比較長;
- 比較難防禦 DoS 攻擊,因為攻擊者可能會大量訪問新頁面,導致 Builder 被大量并行地運行,這裡需要平台方實現 Builder 的歸一化和串行運行。
總結
Jamstack 這套技術棧在國外的流行,很大程度上得益於近年來相關雲服務和雲平台的成熟:
- 新一代的 CDN 技術,包括更高級、更精細的緩存控制能力;
- Serverless 雲函數,讓 SSR 和 SSG 免於服務器運維的苦惱,開發者只需要重點關注前台邏輯;
- 越來越豐富的 BaaS 提供方,提供了包括數據存儲、鑒權、電商、CMS、音視頻、AI 等等「中台化」的能力,開發者只需要組合這些 BaaS 服務,專註於自身的業務邏輯即可。
Jamstack 非常適合以呈現內容為主的網站,如文檔、博客、電商網站、論壇、官網等等,所以更多地應該將它視為」建站技術「,是目前諸多建站技術棧(LAMP、MEAN等等)的一個新生替代品。極低的運維成本、Serverless、快速、安全、且不損失網站的動態性,是它的核心優勢。
當然它本身並不是完美的,SSG、ISR、DPR 這些解決方案,都或多或少有一些瑕疵和問題,它們本質上就是在平衡動態性、渲染性能、緩存性能這三個矛盾點,依然需要繼續探索和演進下去。
另外,除了上文提到的 Netlify 和 Vercel 這兩家小而美的平台以外,國外的幾家大型雲廠商(GCP、AWS、Azure)也提供了類似的產品,向 Web 前端開發者提供對 Jamsatck 等新生代技術棧的支持:
國內市場上,這塊產品目前還處於缺位的狀態,雖然底層的 IaaS 能力(對象存儲、CDN、Serverless、網關等等)都趨近於完善,但還缺少能夠把這些能力組合封裝起來的一層。
當然除了技術層面的原因外,國內外的市場、網絡環境、技術生態都是完全不同的,僅僅是 「Copy to China」 的方式很可能會導致產品水土不服,不過這就超出本文的範疇了,可以後續安排一篇文章詳細聊聊。