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

從零搭建全??梢暬笃林谱髌脚_(tái)V6.Dooring
Dooring可視化搭建平臺(tái)數(shù)據(jù)源設(shè)計(jì)剖析
基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進(jìn)階實(shí)戰(zhàn)
點(diǎn)個(gè)在看你最好看

