React Server Component: 混合式渲染

来源:https://www.cnblogs.com/ClientInfra/archive/2022/11/29/16935937.html
-Advertisement-
Play Games

相信大家對 React Server Component 有所耳聞,React 團隊對它是這樣介紹的: zero-bundle-size React Server Components。這是一種實驗性探索,但相信該探索是個未來 React 發展的方向,與 React Server Component... ...


作者:謝奇璇

React 官方對 Server Comopnent 是這樣介紹的: zero-bundle-size React Server Components。

這是一種實驗性探索,但相信該探索是個未來 React 發展的方向,與 React Server Component 相關的周邊生態正在積極的建設當中。

術語介紹

在 React Server Component (以下稱 Server Component) 推出之後,我們可以簡單的將 React 組件區分為以下三種:

Server Component 服務端渲染組件,擁有訪問資料庫、訪問本地文件等能力。無法綁定事件對象,即不擁有交互性。
Client Component 客戶端渲染組件,擁有交互性。
Share Component 既可以在服務端渲染,又可以在客戶端渲染。具體如何渲染取決於誰引入了它。當被服務端組件引入的時候會由服務端渲染當被客戶端組件引入的時候會由客戶端渲染。

React 官方暫定通過「文件名尾碼」來區分這三種組件:

  1. Server Component 需要以 .server.js(/ts/jsx/tsx) 為尾碼
  2. Client Component 需要以 .client.js(/ts/jsx/tsx) 為尾碼
  3. Share Component 以 .js(/ts/jsx/tsx) 為尾碼

混合渲染

簡單來說 Server Component 是在服務端渲染的組件,而 Client Component 是在客戶端渲染的組件。

與類似 SSR , React 在服務端將 Server Component 渲染好後傳輸給客戶端,客戶端接受到 HTML 和 JS Bundle 後進行組件的事件綁定。不同的是:Server Component 只進行服務端渲染,不會進行瀏覽器端的 hyration(註水),總的來說頁面由 Client Component 和 Server Component 混合渲染。

這種渲染思路有點像 Islands 架構,但又有點不太一樣。

如圖:橙色為 Server Component, 藍色為 Client Component 。

React 是進行混合渲染的?

React 官方提供了一個簡單的 Demo , 通過 Demo,探索一下React sever component的運作原理。

渲染入口

瀏覽器請求到 HTML 後,請求入口文件 - main.js, 裡面包含了 React Runtime 與 Client Root,Client Root 執行創建一個 Context,用來保存客戶端狀態,與此同時,客戶端向服務端發出 /react 請求。

// Root.client.jsx 偽代碼

function Root() {
    const [data, setData] = useState({});
    
    // 向服務端發送請求
    const componentResponse = useServerResponse(data);
    return (
        <DataContext.Provider value={[data, setData]}> 
            componentResponse.render();
        </DataContext.Provider>
    );
}

看出這裡沒有渲染任何真實的 DOM, 真正的渲染會等 response 返回 Component 後才開始。

請求服務端組件

Client Root 代碼執行後,瀏覽器會向服務端發送一個帶有 data 數據的請求,服務端接收到請求,則進行服務端渲染。

服務端將從 Server Component Root 開始渲染,一顆混合組件樹將在服務端渲染成一個巨大的 VNode。

如圖,這一顆混合組件樹會被渲染成這樣一個對象,它帶有 React 組件所有必要的信息。

module.exports = {
    tag: 'Server Root',
    props: {...},
    children: [
        { tag: "Client Component1", props: {...}: children: [] },
        { tag: "Server Component1", props: {...}: children: [
            { tag: "Server Component2", props: {...}: children: [] },
            { tag: "Server Component3", props: {...}: children: [] },
        ]}
    ]
}

不僅僅是這樣一個對象, 由於 Client Comopnent 需要 Hydration, React 會將這部分必須要的信息也返回回去。React 最終會返回一個可解析的 Json 序列 Map。

M1:{"id":"./src/BlogMenu.client.js","chunks":["client0"],"name":"xxx"}
J0:["$","main", null, ["]]
  • M:  代表 Client Comopnent 所需的 Chunk 信息
  • J:  代表 Server Compnent 渲染出的類 react element格式的字元串

React Runtime 渲染

組件數據返回給瀏覽器後,React Runtime 開始工作,將返回的 VNode 渲染出真正的 HTML。與此同時,發出請求,請求 Client Component 所需的 JS Bundle。當瀏覽器請求到 Js Bundle 後,React 就可以進行選擇性 Hydration(Selective Hydration)。需要註意的是, React 團隊傳輸組件數據選擇了流式傳輸,這也意味著 React Runtime 無需等待所有數據獲取完後才開始處理數據。

啟動流程

  1. 瀏覽器載入 React Runtime, Client Root 等 js 代碼
  2. 執行 Client Root 代碼,向服務端發出請求
  3. 服務端接收到請求,開始渲染組件樹
  4. 服務端將渲染好的組件樹以字元串的信息返回給瀏覽器
  5. React Runtime 開始渲染組件且向服務端請求 Client Component Js Bundle 進行選擇性 Hydration(註水)

Client <-> Server 如何通信?

Client Component 與 Server Component 有著天然的環境隔離,他們是如何互相通信的呢?

Server Component -> Client Component

在服務端的渲染都是從 Server Root Component 開始的,Server Component 可以簡單的通過 props 向 Client Component 傳遞數據。

import ClientComponent from "./ClientComponent";

const ServerRootComponent = () => {
    return <ClientComponent title="xxx" />
};

但需要註意的是:這裡傳遞的數據必須是可序列化的,也就是說你無法通過傳遞 Function 等數據。

Client Component  -> Server Component

Client Component 組件通過 HTTP  向服務端組件傳輸信息。Server Component 通過 props 的信息接收數據,當 Server Component 拿到新的 props 時會進行重新渲染, 之後通過網路的手段發送給瀏覽器,通過 React Runtime 渲染在瀏覽器渲染出最新的 Server Component UI。這也是 Server Component 非常明顯的劣勢:渲染流程極度依賴網路。

// Client Component
function ClientComponent() {
    const sendRequest = (props) => {
        const payload = JSON.stringify(props);
        fetch(`http://xxxx:8080/react?payload=${payload}`)
    }
    return (
        <button 
           onclick = {() => sendRequest({ messgae: "something" })}
        >
            Click me, send some to server
        </button>
    )
}
// Serve Component
const ServerRootComponent = ({ messgae: "something" }) => {
    return <ClientComponent title="xxx" />
};

Server Component 所帶來的優勢

RSC 推出的背景是 React 官方想要更好的用戶體驗,更低的維護成本,更高的性能。通常情況下這三者不能同時獲得,但 React 團隊覺得「小孩子才做選擇,我全都要」。

根據官方提出 RFC: React Server Components,可以通過以下幾點能夠看出 React 團隊是如何做到"全都要"的:

更小的 Bundle 體積

通常情況下,我們在前端開發上使用很多依賴包,但實際上這些依賴包的引入會增大代碼體積,增加 bundle 載入時間,降低用戶首屏載入的體驗。

例如在頁面上渲染 MarkDown ,我們不得不引入相應的渲染庫,以下麵的 demo 為例,不知不覺我們引入了  240 kb 的 js 代碼,而且往往這種大型第三方類庫是沒辦法進行 tree-shaking。

// NOTE: *before* Server Components

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

可以想象,為了某一個計算任務,我們需要將大型 js 第三方庫傳輸到用戶瀏覽器上,瀏覽器再進行解析執行它來創造計算任務的 runtime, 最後才是計算。從用戶的角度來講:「我還沒見到網頁內容,你就占用了我較大帶寬和 CPU 資源,是何居心」。然而這一切都是可以省去的,我們可以利用 SSR 讓 React 在服務端先渲染,再將渲染後的 html 發送給用戶。從這一方面看,Server Component 和 SSR 很類似,但不同的是 SSR 只能適用於首頁渲染,Server Component 在用戶交互的過程中也是服務端渲染,Server Component 傳輸的也不是 html 文本,而是 json。Server Component 在服務端渲染好之後會將一段類 React 組件 json 數據發送給瀏覽器,瀏覽器中的 React Runtime 接收到這段 json  數據 後,將它渲染成 HTML。

我們舉一個更加極端的例子:若用戶無交互性組件,所以組件都可以在服務端渲染,那麼所有 UI 渲染都將走「瀏覽器接收"類 react element 文本格式"的數據,React Runtime 渲染」的形式進行渲染。 那麼除了一些 Runtime, 我們無需其他 JS Bundle。而 Runtime 的體積是不會隨著項目的增大而增大的,這種常數繫數級體積也可以稱為 "Zero-Bundle-Size"。因此官方這稱為: "Zero-Bundle-Size Components"。

更好的使用服務端能力

為了獲取數據,前端通常需要請求後端介面,這是因為瀏覽器是沒辦法直接訪問資料庫的。但既然我們都藉助服務端的能力了,那我們當然可以直接訪問資料庫,React 在伺服器上將數據渲染進組件。

通過自由整合後端能力,我們可以解決:「網路往返過多」和「數據冗餘」問題。甚至我們可以根據業務場景自由地決定數據存儲位置,是存儲在記憶體中、還是存儲在文件中或者存儲在資料庫。除了數據獲取,還可以再開一些"腦洞"。

  • 我們可以在 Server Component 的渲染過程中將一些高性能計算任務交付給其他語言,如 C++,Rust。這不是必須的,但你可以這麼做。
  • ......

簡單粗暴一點的說:Nodejs 擁有什麼樣的能力,你的組件就能擁有什麼能力。

更好的自動化 Code Split

在過去,我們可以通過 React 提供的 lazy + Suspense 進行代碼分割。這種方案在某些場景(如 SSR)下無法使用,社區比較成熟的方案是使用第三方類庫 @loadable 。然而無論是使用哪一種,都會有以下兩個問題:

  1. Code Split 需要用戶進行手動分割,自行確認分割點。
  2. 與其說是 Code Split,其實更偏向懶載入。也就是說,只有載入到了代碼切割點,我們才會去即時載入所切割好的代碼。這裡還是存在一個載入等待的問題,削減了code split給性能所帶來的好處。

React核心團隊所提出 Server Component 可以幫助我們解決上面的兩個問題。

  1. React Server Component 將所有 Client Component 的導入視為潛在的分割點。也就是說,你只需要正常的按分模塊思維去組織你的代碼。React 會自動幫你分割
import ClientComponent1 from './ClientComponent1';


function ServerComponent() {
    return (
        <div>
            <ClientComponent1 />
        </div>
    )
}
  1. 框架側可以介入 Server Component 的渲染結果,因此上層框架可以根據當前請求的上下文來預測用戶的下一個動作,從而去「預載入」對應的js代碼。

避免高度抽象所帶來的性能負擔

React server component通過在伺服器上的實時編譯和渲染,將抽象層在伺服器進行剝離,從而降低了抽象層在客戶端運行時所帶來的性能開銷。

舉個例子,如果一個組件為了可配置行,被多個 wrapper 包了很多層。但事實上,這些代碼最終只是渲染為一個<div>。如果把這個組件改造為 server component 的話,那麼我們只需要往客戶端返回一個<div>字元串即可。下麵例子,我們通過把這個組件改造為server component,那麼,我們大大降低網路傳輸的資源大小和客戶端運行時的性能開銷:

// Note.server.js
// ...imports...

function Note({id}) {
  const note = db.notes.get(id);
  return <NoteWithMarkdown note={note} />;
}

// NoteWithMarkdown.server.js
// ...imports...

function NoteWithMarkdown({note}) {
  const html = sanitizeHtml(marked(note.text));
  return <div ... />;
}

// client sees:
<div>
  <!-- markdown output here -->
</div>

參考自:
https://juejin.cn/post/6918602124804915208#heading-5

我們可以通過在 Server Component ,將 HOC 組件進行渲染,可能渲染到最後只是一個 <div> 我們就無需將 bundle 傳輸過去,也無需讓瀏覽器消耗性能去渲染。

Sever Component 可能存在的劣勢

弱網情況下的交互體驗

如上文所述: React Server Component 的邏輯, 他的渲染流程依靠網路。服務端渲染完畢後將類 React 組件字元串的數據傳輸給瀏覽器,瀏覽器中的 Runtime React 再進行渲染。顯然,在弱網環境下,數據傳輸會很慢,渲染也會因為網速而推遲,極大的降低了用戶的體驗。Server Component 比較難能可貴的是,它跟其他技術並不是互斥的,而是可以結合到一塊。例如:我們完全可以將 Server Component 的計算渲染放在邊緣設備上進行計算,在一定程度上能給降低網路延遲帶來的問題。

開發者的心智負擔

在 React Server Component 推出之後,開發者在開發的過程中需要去思考: 「我這個組件是 Server Component 還是 Client Component」,在這一方面會給開發者增加額外的心智負擔,筆者在寫 Demo 時深有體會,思維上總是有點不習慣。Nextjs 前一段時間發佈了 v13,目前已實現了 Server & Client Component 。參考 Next13 的方案,預設情況下開發者開發的組件都是 Server Component ,當你判斷這個組件需要交互或者調用 DOM, BOM 相關 API 時,則標記組件為 Client Component。

「預設走 Server Component,若有交互需要則走 Client Component」 通過這種原則,相信在一定程度上能給減輕開發者的心智負擔。

應用場景: 文檔站

從上面我們可以知道 Server Component 在輕交互性的場景下能夠發揮它的優勢來,輕交互的場景一般我們能想到文檔站。來看一個小 Demo, 通過這個 Demo 我們觀察到幾個現象:

  1. 極小的 Js bundle。
  2. 文件修改無需 Bundle。

當然像文檔站等偏向靜態的頁面更適合 SSR, SSG,但就像前面所說的它並不與其他的技術互斥,我們可以將其進行結合,更況且他不僅僅能應用於這樣的靜態場景。

參考文檔


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 溫馨提示,請使用ctrl+F進行快速查找 ws2_32.lib error LNK2001: 無法解析的外部符號 __imp_htons error LNK2001: 無法解析的外部符號 __imp_ntohl error LNK2001: 無法解析的外部符號 __imp_ntohs error L ...
  • 1、什麼是MQTT? MQTT(Message Queuing Telemetry Transport,消息隊列遙測傳輸協議),是一種基於發佈/訂閱(publish/subscribe)模式的"輕量級"通訊協議,該協議構建於TCP/IP協議上,由IBM在1999年發佈。MQTT最大優點在於,可以以極 ...
  • 1.玻利維亞 MOPSV 為 5G 移動服務分配 3.3-3.6 GHz 頻段 https://www.oopp.gob.bo/wp-content/uploads/2022/10/2022-RM-174-Modificacion-al-Plan-Nacional-de-Frecuencia.pdf ...
  • 視圖 create view ... as ps:SQL文件在上一篇博客末尾 視圖就是通過查詢得到一張虛擬表,然後保存下來,下次直接使用 create view teacher_course as select * from teacher inner join course on teacher. ...
  • 10.1 事務的基本概念: 什麼是事務?事務是用戶定義的一個資料庫操作序列,該操作要麼全做,要麼全不做,是一個不可分割的工作單位,是恢復(知識點)和併發控制(知識點)的基本單位 事務和程式的區別: 在關係資料庫中,一個事務可以是一條SQL語句,或多條SQL語句,或整個程式 一個程式可以有多個事務 事 ...
  • 本篇開啟資料庫在工作中常用到的格式轉換與工具,歡迎大家評論留言:smile: SQL將小數轉為保留兩位的百分數 CONCAT(CONVERT((<需要轉換的值>)*100,DECIMAL(18,2)),'%') turnNum 常用的日期格式化 引用的是CSDN博主isTrueLoveColour的 ...
  • 案例介紹 歡迎來到我的小院,我是霍大俠,恭喜你今天又要進步一點點了!我們來用JavaScript相關知識,做一個隨機點名的案例。你可以通過點擊開始按鈕控制上方名字的閃動,點擊停止按鈕可以隨機選定一個名字。 案例演示 運行程式後,我們可以看到一個矩形框按鈕,顯示開始點名,點擊後名字隨機閃動。同時按鈕變 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 對於前端人員來講,最令人頭疼的應該就是頁面性能了,當用戶在訪問一個頁面時,總是希望它能夠快速呈現在眼前並且是可交互狀態。如果頁面載入過慢,你的用戶很可能會因此離你而去。所以頁面性能對於前端開發者來說可謂是重中之重,其實你如果瞭解頁面 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...