前言
基本上,關於SSR(server side render)和CSR(client side render)的比較,在網路上已經有很多資源了。本文主要是想以code demo的方式,釐清ssr和stream ssr的差別。
基本SSR
如果只是單純的服務端渲染
相較於React SPA的client side render,server side render可以有效減少到客戶端的js bundle size,直接傳已經渲染好的html。這部分的程式碼demo在此 [repo folder]()。同時,以下會略過有關webpack編譯的過程,只著重在跟SSR有關的邏輯。
以下是基於React + NodeJS express的SSR demo:
- 新增一個
app/page.js
,寫一個React組件 import React, { useState } from "react"; export default function MyApp() { const [count, setCount] = useState(0); return ( <div> <h1>Hello SSR, Counters {count} times</h1> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }
新增
server.js
,撰寫SSR相關的運行邏輯。第一段主要是引入了renderToString
這個函數,它用於將React Element render成HTML,可以在SSR時拼接到HTML文本中import express from "express"; import React from "react"; import { renderToString } from "react-dom/server"; import App from "./app/page"; const content = renderToString(<App />);
將轉換好的react element拼接到HTML文本中
` <html> <head> <title>Tiny React SSR</title> </head> <body> <div id='root'> ${content} </div> </body> <script src="/client.bundle.js"></script> </html> `
這樣一來,server side就可以以簡單的 Express api server來返回 HTML文本
const app = express(); app.get("/", (req, res) => res.send(` <html> <head> <title>Tiny React SSR</title> </head> <body> <div id='root'> ${content} </div> </body> </html> `) );
不過,在瀏覽器中可以看到,請求回來的HTML文本中,如果點解button,是沒有辦法觸發任何邏輯。因為在目前的SSR邏輯中,只有把React Element轉化爲實體DOM、但並沒有進行事件綁定。
引入client.bundle.js,進行事件綁定
如果要進行事件綁定,最簡單的方式,就是在客戶端再渲染一次React DOM、直接替代SSR中的節點:
新增
client.js
import React from "react"; import { createRoot } from "react-dom/client"; import App from "./app/page"; const root = createRoot(document.getElementById('root')); root.render(<App />);
webpack.client.js
會將client.js
輸出成client.bundle.js
,讓瀏覽器可以讀取接著只要修改
server.js
中的HTML模板,將client.bundle.js
引入` <html> <head> <title>Tiny React SSR</title> </head> <body> <div id='root'> ${content} </div> </body> <script src="/client.bundle.js"></script> </html> `
這樣一來,就可以在瀏覽器中觸發點擊事件了。當然,這個做法的缺點也是顯而易見:server 渲染了<MyApp/>
一次,然後到了瀏覽器中又渲染一次<MyApp/>
。
Hydrate
react-dom/server 的hydrateRoot
,便是可以複用已有的DOM節點、而不需要重新重複掛載。
修改
client.js
import React from "react"; import { hydrateRoot } from "react-dom/client"; import App from "./app/page"; hydrateRoot(document.getElementById("root"), <App />);
這樣一來,就可以不用完全重新渲染、就可以綁定事件,減少了browser的渲染開銷。
Stream SSR
雖然SSR相較於CSR,可以事先在服務端渲染好再傳送到客戶端,白屏時間減少。不過如果組件過大,也會造成服務端響應耗時過久、讓用戶等待的情形。而 Stream SSR,可以流式傳送已經渲染好的組件、進而加快出始加載時間。這部分的程式碼demo在此 [repo folder]()
在
server.js
中,引入renderToPipeableStream
,此為基於 HTTP/1.1 中的Chunked transfer encoding機制import { renderToPipeableStream } from 'react-dom/server';
在 html字符串中,依據
<div id="root"></div>
切分前後,作為初始渲染和渲染結束的頭與尾。const [head, tail] = htmlData.split('<div id="root"></div>');
主要 stream ssr的邏輯部分
res.write(head + '<div id="root">'); // 寫入頭 const stream = renderToPipeableStream(<App />, { onShellReady() { res.statusCode = 200; stream.pipe(res, {end: false}); // 開始傳輸 }, onShellError(err) { console.error(err); res.statusCode = 500; res.send('Server Error'); res.end(); }, onAllReady() { res.write('</div>' + tail); // 寫入尾 res.end(); }, });
在demo中,簡單寫了3個包含10000個item的list組件,在console中可以看出頁面是在不斷加載、但頁面有先掛載出已經渲染的部分、並沒有一直等待。
可以從瀏覽器的network項目中,查看到網頁是先掛載了主要元素、然後不停渲染流式傳入的dom節點。(此爲啟動了chrome disable cache 和 slow 3G 的設置選項,為了突出stream ssr的優點)
ChangeLog
- 20240401-初稿