Foreword
Currently, there are lots of content related with the difference between SSR(server side render) and CSR(client side render). As a result, this article would just display how SSr and stream SSR work in code leve.
Basic of SSR
if just talking about server side render
Compare with client side rendering (like Reast SPA), server side rendering could reduce the javascript bundle size which sent to the browser, just send the HTML string. Could take the code demo in this [repo folder](). Besides, following content will pass the compile progress of webpack, just focus on SSR related logic.
Please check SSR demo by React + NodeJS express:
create
app/page.js
for a React component: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> ); }
create
server.js
for SSR logic. During importing area, import therenderToString
function, which could transport React element into HTML string, so we could concat the element into HTML template string.import express from "express"; import React from "react"; import { renderToString } from "react-dom/server"; import App from "./app/page"; const content = renderToString(<App />);
and then concat it
` <html> <head> <title>Tiny React SSR</title> </head> <body> <div id='root'> ${content} </div> </body> <script src="/client.bundle.js"></script> </html> `
Now, we could use express server api to return HTML with React element.
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> `) );
However, we cannot trigger the button click function because we only turn React element into dom element without binding event in current process.
import client.bundle.js to bind the event function
If want to make event binding happen, it is the simplest way to render the React DOM again in client side, to replace the node from SSR:
create
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
will turnclient.js
intoclient.bundle.js
, in order to make browser import it.then merely modify the HTML template from
server.js
, import theclient.bundle.js
part:` <html> <head> <title>Tiny React SSR</title> </head> <body> <div id='root'> ${content} </div> </body> <script src="/client.bundle.js"></script> </html> `
So that, could trigger the click function in browser. At the meantime, render <MyApp/>
twice is not the efficient way obviously.
Hydrate
hydrateRoot
function from react-dom/server is to solve the re-render all dom issue by using existed dom.
modify
client.js
import React from "react"; import { hydrateRoot } from "react-dom/client"; import App from "./app/page"; hydrateRoot(document.getElementById("root"), <App />);
Now it could bind the event function without re-render, reduce the cost from browser.
Stream SSR
Although server-side rendering offers the advantage of pre-rendering content on the server before sending to the client, thus reducing the time to first render (offer referred to as the white screen time`)., there’s a caveat. If compenents are too large, it can lead to excessive response times form the server, causing the user to wait longer. However, stream SSR presents a solution by allowing the server to stream components as they are rendered, which can significantly speed up the initial loading time. A demo of this implementation can be found in the [repo folder]()
In
server.js
, importrenderToPipeableStream
. This utilizes the Chunked transfer encoding mechanism based on HTTP/1.1.import { renderToPipeableStream } from 'react-dom/server';
In the HTML string, split the content based on
<div id="root"></div>
to define the beginning and end of initial rendering and rendering completion.const [head, tail] = htmlData.split('<div id="root"></div>');
The core logic of Stream SSR primarily involves:
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(); }, });
In the demo, three simple list components containing 10,000 items each were created. From the console, it can be observed that the page continuouly loads but mounts the already-rendered parts first, without waiting for the entire page to load.
Through the browser’s network tab, it’s evident that the web page first mounts the main elements, and then progressively renders and streams in DOM nodes.( This was observed with Chrome’s disable cache and slow 3G settings enabled, to highlight the advantages of stream SSR)
ChangeLog
- 20240402-init