現代前端框架的渲染模式
Head first
盡管現在看來這系列圖書內容可能過時了。
Head First 系列圖書讓我知道,原來編程也可以這么通俗易懂的,對于剛接觸這個領域的同學來說,從這里可以獲得很多信心和成就感。這種風格也一直影響著我,學習和工作、傳道授業(yè)過程中,我會努力把復雜的事情簡化、通俗化,提煉本質。
這十年,前端渲染方式一直在演進,我覺得大概可以分為以下三個階段:
Untitled- 傳統(tǒng) SSR: 那時候前端還沒有分離,在 JSP、ASP、Ruby on Rails、Django 這些 MVC 框架下,通過模板來渲染頁面。jQuery 是這個階段的主角
- 前后端分離:從 Node.js 發(fā)布,到目前為止,是前端發(fā)展最迅速的 10 年。前后端分離的典型代表是 Angular 和 React、Vue 等框架,我覺得,促進前后端分離的主要原因還是隨著需求的復雜化,分工精細化了。前端可以專注于 UI 的設計和交互邏輯。后端只需要提供 API,不需要關心前端的具體實現。
-
同構前端:這幾年前端框架的發(fā)展進入的深水區(qū),隨著云原生、容器技術、Serverless、邊緣計算等底層技術設施的普及,也讓‘前端’生存范圍延展到服務端。前端開始尋求
UX和DX的平衡點
通過這篇文章,你就可以知道近些年前端渲染模式的演變。廢話不多說,直接開始吧。
CSR - 客戶端渲染
Untitled這個我們再熟悉不過了, 即前端頁面在瀏覽器中渲染,服務端僅僅是靜態(tài)資源服務器(比如 nginx)。
初始的 HTML 文件只是一個空殼,我們需要等待 JavaScript 包加載和執(zhí)行完畢,才能進行交互,白屏時間比較長。
-
優(yōu)點
- 部署簡單
- 頁面過渡、功能交互友好
- 適合復雜交互型應用程序開發(fā)
-
缺點
-
SEO不友好 - 白屏時間長
- 可能需要復雜的狀態(tài)管理。時至今日,狀態(tài)管理方面的輪子還在不停地造
-
SSR - 服務端渲染
Untitled為了解決 SEO 和白屏問題,各大框架開始支持在服務端渲染 HTML 字符串。
SSR 把數據拉取放到了服務端,因為離數據源比較近,數據拉取的速度會快一點。但這也不是完全沒有副作用,因為需要在服務端等待數據就緒, TTFB(Time to First Byte) 相比 CSR 會長一點。
SSR 只是給我們準備好了初始的數據和 HTML, 實際上和 CSR 一樣,我們還是需要加載完整的客戶端程序,然后在瀏覽器端重新渲染一遍(更專業(yè)的說是 Hydration 水合/注水),才能讓 DOM 有交互能力。
也就說, FCP(First Contentful Paint) 相比 CSR 提前了, 但是 TTI(Time to Interactive) 并沒有太多差別。只是用戶可以更快地看到內容了。
hydration 的主要目的是掛載事件處理器、觸發(fā)副作用等等
優(yōu)點
- SEO 友好
- 用戶可以更快看到內容了
缺點
- 部署環(huán)境要求。需要 Nodejs 等 JavaScript 服務端運行環(huán)境
-
需要包含完整的 JavaScript 客戶端渲染程序,
TTI還有改善空間
SSG - 靜態(tài)生成
Untitled對于完全靜態(tài)的頁面,比如博客,公司主頁等等,也可以使用 SSG 靜態(tài)渲染。
和 SSR 的區(qū)別是,SSG 是在構建時渲染的。
和 CSR 一樣,因為是靜態(tài)的,所以在服務端不需要渲染運行時,部署在靜態(tài)服務器就行了。
VuePress、VitePress、Gatsby、Docusaurus 這些框架都屬于 SSG 的范疇。
優(yōu)點
- 相比 SSR, 因為不需要服務端運行時、數據拉取,TTFB/FCP 等都會提前。
缺點
-
和 SSR 一樣,也有客戶端渲染程序、需要進行 Hydrate。對于
內容為中心的站點來說,實際上并不需要太多交互,客戶端程序還有較大壓縮的空間。 - 在構建時渲染,如果內容變更,需要重新構建,比較麻煩
ISG - 增量靜態(tài)生成
UntitledISG 是 SSG 的升級版。解決 SSG 內容變更繁瑣問題。
ISG 依舊會在構建時預渲染頁面,但是這里多出了一個服務端運行時,這個運行時會按照一定的過期/刷新策略(通常會使用 stale-while-revalidate )來重新生成頁面。
Progressive Hydration - 漸進水合
Untitled上文提到,常規(guī)的 SSR 通常需要完整加載客戶端程序(上圖的 bundle.js),水合之后才能得到可交互頁面,這就導致 TTI 會偏晚。
最直接的解決辦法就是壓縮客戶端程序的體積。那么自然會想到使用代碼分割(code splitting)技術。漸進式水合 (Progressive Hydration ) 就是這么來的。
如上圖,我們使用代碼分割的方式,將 Foo、Bar 抽取為異步組件,抽取后主包的體積下降了,TTI 就可以提前了。
而 Foo、Bar 可以按照一定的策略來按需加載和水合,比如在視口可見時、瀏覽器空閑時,或者交給 React Concurrent Mode 根據交互的優(yōu)先級來加載。
React 18 官方支持了漸進式水合(官方叫 Selective Hydration)。
要深入了解 Progress Hydration, 可以看這個視頻。
SSR with streaming - 流式 SSR
Untitled這個很好理解。尤其是在最近 ChatGPT 這么火。ChatGPT API 有兩種響應模式:普通響應、流式響應
- renderToString → 普通響應。即 SSR 會等待完整的 HTML 渲染完畢后,才給客戶端發(fā)送第一個字節(jié)。
- renderToNodeStream → 流式響應。渲染多少,就發(fā)送多少。就像 ChatGPT 聊天消息一樣,一個字一個字的蹦,盡管接收完整消息的時間可能差不多,用戶體驗卻相差甚遠。
瀏覽器能夠很好地處理 HTML 流,快速地將內容呈現給用戶,而不是白屏干等。
下面這張圖可以更直觀感受兩者區(qū)別:
來源:https://mxstbr.com/thoughts/streaming-ssr/來源:https://mxstbr.com/thoughts/streaming-ssr/
對于常規(guī)的流式 SSR,優(yōu)化效果可能沒有我們想象的那么明顯。因為框架還是得等數據拉取完成之后才能開始渲染。因此,除非是比較復雜、長序列的 HTML 樹,至上而下需要較長時間的渲染,否則效果并不明顯。
優(yōu)點
- 相比普通響應,流式響應可以提前 TTFB 和 FCP, 瀏覽器不用空轉等待,可以連續(xù)繪制。
缺點
-
數據拉取是 TTFB/FCP 的主要阻塞原因。為了解決這個問題,下文的
Selective Hydration如何巧妙地解決這個問題。
Selective Hydration - 選擇性水合
Untitled選擇性水合(Progressive Hydration) 是 漸進式水合(Progressive Hydration) 和 流式SSR(SSR with Streaming) 的升級版。主要通過選擇性地跳過‘慢組件’,避免阻塞,來實現更快的 HTML 輸出, 從而讓流式響應發(fā)揮應有的作用。
慢組件通常指的是:需要異步獲取數據、體積較大、或者是計算量比較復雜的組件。
比較典型的慢組件是異步數據獲取的組件, 如下圖,未開啟 Selective Hydration 的情況,會等待所有異步任務完成后才開始輸出,而 Selective Hydration 可以跳過這些組件,等待它們就緒后,繼續(xù)輸出。
Untitled我們可以在最新的 Next.js(當前是 13.4) 演示一下。
沒有開啟 Selective Hydration 的 Demo:
function delay(time: number) {
return new Promise((resolve) => setTimeout(resolve, time))
}
/**
* 獲取關鍵數據
*/
function getCrucialData() {
return delay(1000).then(() => {
return {
data: Math.random(),
}
})
}
function getData(time: number) {
return delay(time).then(() => {
return {
data: Math.random(),
}
})
}
const Foo = async () => {
const data = await getData(1000)
return foo: {data.data}
}
const Bar = async () => {
const data = await getData(2000)
return bar: {data.data}
}
/**
* 頁面 ??
*
*/
export default async function WithoutSelective() {
// 獲取關鍵數據
const crucialData = await getCrucialData()
return (
Without Selective
This page is rendered without Selective Hydration.
crucial data: {crucialData.data}
)
}
運行結果:瀏覽器等待響應的時間為 3s
即所有服務端組件(Server Component) 就緒后才會有實際的內容輸出。
開啟 Selective Hydration 很簡單,我們只需要用 Suspend 包裹起來,提示 React 這可能是一個‘慢組件’,可以跳過他:
export default async function WithoutSelective() {
// 獲取關鍵數據
const crucialData = await getCrucialData()
return (
Without Selective
This page is rendered without Selective Hydration.
crucial data: {crucialData.data}
)
}
現在來看運行結果:
Untitled明顯 TTFB 提前了!但是完整的請求時間沒變。
當 Foo 和 Bar 就緒后,Next.js 會將渲染結果寫入流中。怎么做到的?
看一眼 HTML 就知道了:
Untitled對于慢組件,React 會先渲染 Suspend 的 fallback 內容,并留一個插槽。
繼續(xù)往下看,可以看到 Foo、Bar 的渲染結果:
Untitled接著將渲染結果替換掉插槽。用于后續(xù)的水合。
總之,在服務端,Selective Hydration 在 SSR With Streaming 的基礎上,通過選擇性地跳過一些低優(yōu)先級的慢組件來優(yōu)化了 TTFB(主要的,相對于 FCP 等指標也優(yōu)化了),更快地向用戶呈現頁面。
在客戶端 Selective Hydration 的運行過程同 Progressive Hydration 。
關于 Selective Hydration 細節(jié),可以閱讀以下文章:
- New in 18: Selective Hydration
- New Suspense SSR Architecture in React 18
Islands Architecture - 島嶼架構
Untitled近兩年,去 JavaScript 成為一波小趨勢,這其中的典型代表是 Islands Architecture (島嶼架構)和 React Server Component(RSC, React 服務端組件)。
它們主張是:在服務端渲染,然后去掉不必要 JavaScript
島嶼架構的主要代表是 Astro。如上圖,Astro 在服務端渲染后,默認情況下,在客戶端側沒有客戶端程序和水合的過程。而對于需要 JavaScript 增強,實現動態(tài)交互的組件,需要顯式標記為島嶼。
這有點類似 Progressive Hydration 的意思。但是還是有很大的差別:
-
島嶼是在
去 JavaScript這個背景下的交互增強手段。按 Astro 解釋是:你可以將‘島嶼’想象成在一片由靜態(tài)(不可交互)的 HTML 頁面中的動態(tài)島嶼 - 每個島嶼都是獨立加載、局部水合。而 Progressive Hydration 是整棵樹水合的分支,只不過延后了。
- 島嶼可以框架無關。
去 JavaScript 后,可以緩解典型的 SSR TTI 問題。但是島嶼架構并不能通吃所有的場景,最擅長的是”內容為中心“的站點,即當靜態(tài)的頁面比重遠高于動態(tài)比重時,去 JavaScript 的收益才是顯著的。
React Server Component - React 服務端組件
Untitled在筆者看來,React Server Component(RSC) 本質上和島嶼架構的目的是一樣的,都是去 JavaScript。只是實現的手段不同。
這是 Next.js 官方文檔的示例圖:和島嶼架構類似,對于靜態(tài)的內容推薦使用 Server Component (SC), 而需要交互增強的,可以使用 Client Component (CC)。
Untitled顧名思義,RSC 就是只能在服務端運行的組件。下面簡單對比一下兩者的區(qū)別:
|
|
Server Component | Client Component |
|---|---|---|
| 運行環(huán)境 | 服務端 | - 服務端 + 客戶端 |
| - 僅客戶端 |
|
|
| JavaScript | 服務端組件依賴的相關程序對客戶端不可見。 |
|
| 在這里實現了 ‘去 JavaScript’ | 需要打包分發(fā)給客戶端 |
|
| 水合 | 不需要水合 | 需要水合 |
| 支持 async | Y | N |
| 支持狀態(tài)(state, context) | N | Y |
| 支持事件、副作用 | N | Y |
RSC 優(yōu)點類似 React Hooks 出來之前的函數組件: 就是一個普通的函數,不能使用 hooks,沒有狀態(tài),只會被調用一次。
你可以通過 Next.js 的文檔,深入學習 RSC。React 官方的討論組也是不錯的一手學習場地。
那么相比島嶼架構呢?
優(yōu)點
- Server Component 和 Client Component 都是 React 框架的組件,盡管有些區(qū)別,但是心智模型是統(tǒng)一的。
- React Server Component 是 React 框架下一體化的原生解決方案,支持和 Selective Hydration 配合使用。島嶼架構只是一個架構模式。
- 可以進行更細粒度和更靈活的組合。
缺點
- Server Component 和 Client Component 還是有較大差別,在組合、通信上也有較多限制,需要開發(fā)者規(guī)劃好服務端和客戶端的邊界。初期有一定上手門檻。當然,Islands 可能也有類似的問題。
總結
本文篇幅較長,我給大家整理了這些渲染模式的發(fā)展歷程和關系脈絡
Untitled任何技術的迭代都是有其動機和脈絡。不推薦大家面向熱度編程,大部分情況下,做到‘知其然,也知其所以然’,就足夠了。
擴展閱讀
本文主要參考的內容來源是patterns.dev。這個網站收錄了許多實用的前端設計模式,大家趕緊收藏起來!
- Pattern dev
- Next.js
- Next.js Incremental Static RegenerationExamples
- reactwg/server-components
- Is 0kb of JavaScript in your Future?
- Islands Architecture
以上便是本次分享的全部內容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點贊、收藏 三連哦~。

基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進階實戰(zhàn)
點個在看你最好看

