寫在前面
2020 年的聖誕節前, React 團隊放出了 Server Components 的相關消息,而此前,我恰好在研究 SSR(Server-Side Rendering,伺服器端渲染),並對 Next.js 的混合渲染 讚嘆不已
其實 Server Components 也確實與 Server-Side Rendering 有著千絲萬縷的聯繫,畢竟兩者都帶個 Server 嘛(認真臉,這一點很重要)。得益於此前對 SSR 的一系列研究,所以,從某種程度上來講,我更能夠深刻理解 Server Components 背後的思考和設計考量
一.Server Components 是什麼?
Server Components are a new type of?component that we're introducing to?React and this tells React that we only want?this component to ever render on the server.
(摘自 Data Fetching with React Server Components 演講影片)
官方定義, Server Components 本質上是一種新的 React 組件(像 Portal、Fragment 等組件類型一樣),其特別之處在於這種組件只在伺服器端執行
可是, React 組件在用戶端跑得好好的,怎麼突然說要拿到伺服器端去執行呢? Hooks 還沒學明白,一大波伺服器端組件的更新又要來了, React 團隊這幫人到底在搞什麼?
二.為了解決什麼問題?
按 RFC 上寫的, React 引入 Server Components 概念能夠解決一大把問題:
-
Zero-Bundle-Size Components:打包產物大小的問題
-
Full Access to the Backend:數據存取、傳遞需要先考慮組件樹結構的問題
-
Automatic Code Splitting:程式碼拆分的效能優化需要手動改造的問題
-
No Client-Server Waterfalls:請求順序與組件樹結構強關聯的問題
-
Avoiding the Abstraction Tax:多層抽象帶來的效能問題
諸如此類,但這些看起來更像是從解決方案倒推出來的收益,而我更關注的是其初衷,最初最想解決的問題是什麼?
First, we wanted to make it easier for developers to fall into the “pit of success” and achieve good performance by default. Second, we wanted to make it easier to fetch data in React apps.
(摘自 Motivation )
初衷是想解決兩大類問題:
-
第一類:效能優化相對複雜,能否預設就實現高興能?
-
第二類:在 React 應用中取數據其實是有不少顧慮的,有沒有更簡單、更優雅的辦法?
效能優化比如按需引用類別庫、按路由拆分程式碼、數據請求提前、減少過度抽象等等,這些優化措施都需要手動改造,給應用開發造成了一定的複雜性。另一方面,為了高興能,通常把數據請求提升到頂層,導致數據展示組件無法清晰地對應到數據源上
例如我們常見的:
// 数据展示组件的数据源依赖不清晰
function ArtistPage({ artistID }) {
const artistData = fetchAllTheStuffJustInCase();
return (
<ArtistDetails
details={artistData.details}
artistId={artistId}>
<TopTracks
topTracks={artistData.topTracks}
artistId={artistId} />
<Discography
discography={artistData.discography}
artistId={artistId} />
</ArtistDetails>
);
}
(出於程式碼維護性考慮)更想要的是:
// 类似这样的清晰依赖,每个组件明确知道其数据从哪来
function ArtistDetails({ artistId, children }) {
const artistData = fetchDetails(artistId);
// ...
}
function TopTracks({ artistId }) {
const topTracks = fetchTopTracks(artistId);
// ...
}
function Discography({ artistId }) {
const discography = fetchDiscography(artistId);
// ...
}
對於不斷追尋極致使用者體驗和開發體驗的 React 團隊而言,更簡單、更優雅的數據獲取方式是其一直以來的探索方向(如 Suspense for Data Fetching (Experimental) )。因此,實際情況可能是 React 團隊在解決數據獲取問題時,提出了 Server Components 的思路,一拍腦門發現這種大膽的想法還能順道解決許多效能問題,於是就有了用戶端到伺服器端的躍遷式新特性預告……
三. Server Components 是怎麼解決的?
要解決組件的數據關聯問題,就要讓組件有各自清晰的數據源,而瀑布式請求又會帶來效能問題……好使用者體驗、低維護成本、高興能似乎難以三者兼具,但也並非不可能兼具,至少 React 團隊已經探索除了兩種解法:
-
Relay + GraphQL: Relay 框架 配合 GraphQL 特性,對散落各處的數據請求進行合併,從而解決效能問題
-
Move our components to the server:把組件搬到伺服器上去執行,降低數據請求的成本,從而(在很大程度上)解決效能問題
GraphQL 能夠根據請求指定的數據模型(schema)輕鬆拼裝數據片段,配合 Relay 框架將多次請求合併成一次,既保留了組件源碼的維護性(清晰的數據源依賴),又避免了由此產生的效能問題,但可惜的是強依賴 GraphQL,不算是一個真正意義上的通用解決方案
而Server Components 的路子相對狂野些,為了降低多次用戶端請求的時間開銷,乾脆把組件放到伺服器上執行,而(同單元)伺服器間的數據通訊是相當快的,這時候多次數據請求的效能開銷便不足為懼了,並且最終會在(框架層)引入數據快取機制後得到徹底解決
等等,把組件搬到伺服器上執行,不就是 SSR 嗎?這莫非是朝花夕拾?
四. Server Components 與 SSR 的關係?
聯繫
一般來講,傳統的 SSR 免不了兩個過程, 伺服器端渲染 + 用戶端 hydrate 二次渲染 :
-
伺服器端渲染:在伺服器端渲染出首屏內容( HTML 字串)
-
用戶端 hydrate 二次渲染:在用戶端載入完首屏內容(此時頁面不可互動)、以及前端應用的完整程式碼之後,進行一次類似於 render 的 hydrate 二次渲染過程,把互動事件綁上去(此時頁面可互動)
Server Components 的渲染過程與之類似:
-
伺服器端渲染:在伺服器端渲染出首屏內容(一種中間形式,同樣是用來描述 UI 的)
-
用戶端渲染:接到伺服器端輸出的中間形式,從頭 render,開始流式渲染
所以, Server Components 與 SSR 的聯繫至少有以下幾點:
- 都在伺服器端執行組件渲染邏輯(所以都不支援互動)
- 都允許同一組件跨用戶端、伺服器端執行( Shared Components )
- 都從用戶端延伸到伺服器端尋求更多的效能突破
對於二者之間的關係, React 官方有一個詞表述得很到位,Server Components 與 SSR 是互補的(complementary),雙劍合璧, SSR 能把首屏渲染成 HTML ���速內容展示, Server Components 能夠幫助減少 hydrate 二次渲染所需載入執行的程式碼量( Server Components 只在伺服器端渲染,相關程式碼不需要在用戶端載入執行),進而加快頁面的可互動時間:
You can combine Server Components and SSR, where Server Components render first, with Client Components rendering into HTML for fast non-interactive display while they are hydrated. When combined in this way you still get fast startup, but you also dramatically reduce the amount of JS that needs to be downloaded on the client.
區別
關鍵區別有 3 點:
-
Server Components 相關程式碼根本不會給到用戶端,而傳統 SSR 所有組件程式碼全都要打進用戶端 bundle 裡
-
Server Components 允許在組件樹的任意位置直接存取後端,傳統 SSR 只允許在頂層(頁面級)獲取數據
-
Server Components 在更新時能保留用戶端互動狀態(包括輸入的搜尋詞、捲動位置、焦點、選中內容等等),因為 Server Components 渲染結果是一種比 HTML 資訊更豐富的中間格式(畢竟 HTML 只能表達 HTML,自定義格式則不存在這個限制,比如能帶上 props )
Server Components 只在伺服器端執行,用戶端並不載入這些程式碼,伺服器端給到用戶端的始終只是 Server Components 的渲染結果,包括二次更新,以中間形式給到用戶端後,用戶端只把來自服務的渲染結果 merge 到當前已經渲染好的用戶端組件上,所以能保留互動狀態:
Specifically, React merges new props passed from the server into existing Client Components, maintaining the state (and DOM) of these components to preserve focus, state, and any ongoing animations.
P.S. 關於二者區別的詳細資訊,見 danabramov on Zero-Bundle-Size React Server Components
五. Server Components 的優勢
1. 有利於減小 bundle size
因為 Server Components 只在伺服器端執行,組件本身及其依賴庫都不打進用戶端 bundle 中,所以能在很大程度上縮減包體積( Facebook 的試點案例減小了 30% 左右 )
另一方面,中間的多層抽象封裝都在伺服器端被消化掉了,減輕了用戶端的負擔
2. 能在組件樹的任意位置存取後端資源
能在組件樹的任意位置存取後端資源,這在傳統 SSR 中也是做不到的,因為傳統 SSR 缺少用戶端框架配合,只能要求數據一次性拿回來,然後進行一次同步的組件渲染,最後將結果給到用戶端
實際上,初衷是為了讓組件與其數據源的關係更清晰,程式碼可維護性更好:
// 类似这样的清晰依赖,每个组件明确知道其数据从哪来
function ArtistDetails({ artistId, children }) {
const artistData = fetchDetails(artistId);
// ...
}
function TopTracks({ artistId }) {
const topTracks = fetchTopTracks(artistId);
// ...
}
function Discography({ artistId }) {
const discography = fetchDiscography(artistId);
// ...
}
修改組件時一同修改對應數據,下線組件時連同數據請求一起下掉
3. 能夠按需下載程式碼
因為伺服器端有數據,確切知道需要下發哪些組件:
Server Components let?you only download the code that you actually need, they enable?automatic code splitting for client code?with support of a bundler plugin, you can use as?much Server Components or as little as you like.
同時讓自動的程式碼拆分成為了可能,所有 Client Components import 自動按需載入,不再需要逐一採用 dynamic imports,開發者不用顯式關注, Server Components 預設支援
4. 與 SSR 互補
單從 SSR 的角度來看,Server Components 是組件化框架從組件系統層面解決了 SSR 應用框架解決不了的問題,比如:
-
加快 hydrate 二次渲染(減少用戶端所需載入執行的程式碼量,曲線救國)
-
流式渲染支援(不同於 SSR 流式輸出,流式渲染一定是需要組件化框架本身配合伺服器端的)
-
允許局部刷新,保留互動狀態(傳統 SSR 只能用作首屏)
這些效能點單靠 SSR 框架是沒有辦法做到極致的,而 Server Components 大大加速了這一進程
另一方面,開篇提到 Next.js 在混合渲染方面進行了深入地探索,允許 SSG、SSR、CSR 以多種方式混用,抓住一切機會進行預渲染,其目的是提升首屏效能(包括 SPA 路由跳轉等互動場景下的首屏效能)。因此,從某種意義上來講,Server Components 與這些預渲染探索殊途同歸,也因此並不衝突能夠配合使用
六. 發展現狀
雖然早在去年就在 Facebook 內部進行了試點,但只是初步驗證,離生產化還有一定距離
並且因為把組件搬到伺服器端去執行,涉及建置、伺服器端渲染、路由控制等諸多環節,超出了組件化框架的範疇,所以 React 團隊計畫與 Next.js 團隊合作共建,先嘗試與 Next.js 進行整合(當然, Server Components 並不僅限於某個特定 SSR 框架使用,只是先整合一個試試)
目前可透過 官方 Demo 進行試玩,需要注意的是,由於 Demo 依賴 postgre 資料庫,建議透過 docker 啟動:
docker-compose up -d
docker-compose exec notes-app npm run seed
透過試玩能了解一些細節,比如 Server Components 渲染而來的中間格式大概長這樣:
[caption id="attachment_2344" align="alignnone" width="625"]
server-components[/caption]
其中, Client Components 以 bundle 索引的形式傳回,原生組件(div、span 等)以 JSON 格式傳回,例如:
[
"$",
"header",
null,
{
"className": "sidebar-note-header",
"children": [
["$", "strong", null, { "children": "todo" }],
["$", "small", null, { "children": "5/4/21" }]
]
}
]
P.S. 關於 React Server Components 的更多技術細節,見 RFC: React Server Components
暫無評論,快來發表你的看法吧