[Frontend Note]What we talk about when we talk about SSR

The server side render from NextJS

Posted by Jamie on Monday, April 1, 2024

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 the renderToString 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 turn client.js into client.bundle.js, in order to make browser import it.

  • then merely modify the HTML template from server.js, import the client.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, import renderToPipeableStream. 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)

image

ChangeLog

  • 20240402-init