Server-side rendering (SSR)

This chapter introduces how to implement SSR functionality using Rsbuild.

Please note that Rsbuild itself does not provide out-of-the-box SSR functionality, but instead provides low-level APIs and configurations to allow framework developers to implement SSR. If you require out-of-the-box SSR support, you may consider using full-stack frameworks based on Rsbuild, such as Modern.js.

What is SSR

SSR stands for "Server-side rendering". It means that the HTML of the web page is generated by the server and sent to the client, rather than sending only an empty HTML shell and relying on JavaScript to generate the page content.

In traditional client-side rendering, the server sends an empty HTML shell and some JavaScript scripts to the client, and then fetching data from the server's API and fills the page with dynamic content. This leads to slow initial page loading times and is not conducive to user experience and SEO.

With SSR, the server generates HTML that already contains dynamic content and sends it to the client. This makes the initial page loading faster and more SEO-friendly, as search engines can crawl the rendered page.

File structure

A typical SSR application will have the following files:

- index.html
- server.js          # main application server
- src/
  - App.js           # exports app code
  - index.client.js  # client entry, mounts the app to a DOM element
  - index.server.js  # server entry, renders the app using the framework's SSR API

The index.html will need to include a placeholder where the server-rendered rendered should be injected:

<div id="root"><!--app-content--></div>

Create SSR configuration

In the SSR scenario, two types of outputs (web and node targets), need to be generated at the same time, for client-side rendering (CSR) and server-side rendering (SSR) respectively.

At this time, you can use Rsbuild's multi-environment builds capability, define the following configuration:

rsbuild.config.ts
export default {
  environments: {
    // Configure the web environment for browsers
    web: {
      source: {
        entry: {
          index: './src/index.client.js',
        },
      },
      output: {
        // Use 'web' target for the browser outputs
        target: 'web',
      },
      html: {
        // Custom HTML template
        template: './index.html',
      },
    },
    // Configure the node environment for SSR
    node: {
      source: {
        entry: {
          index: './src/index.server.js',
        },
      },
      output: {
        // Use 'node' target for the Node.js outputs
        target: 'node',
      },
    },
  },
};

Custom server

Rsbuild provides the Dev server API and environment API to allow you to implement SSR.

Here is a basic example:

server.mjs
import express from 'express';
import { createRsbuild } from '@rsbuild/core';

async function initRsbuild() {
  const rsbuild = await createRsbuild({
    rsbuildConfig: {
      server: {
        middlewareMode: true,
      },
    },
  });
  return rsbuild.createDevServer();
}

async function startDevServer() {
  const app = express();
  const rsbuild = await initRsbuild();
  const { environments } = rsbuild;

  // SSR when accessing /index.html
  app.get('/', async (req, res, next) => {
    try {
      // Load server bundle
      const bundle = await environments.node.loadBundle('index');
      const template = await environments.web.getTransformedHtml('index');
      const rendered = bundle.render();
      // Insert rendered content into HTML template
      const html = template.replace('<!--app-content-->', rendered);

      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(html);
    } catch (err) {
      logger.error('SSR failed: ', err);
      next();
    }
  });
  app.use(rsbuild.middlewares);

  const server = app.listen(rsbuild.port, async () => {
    await rsbuild.afterListen();
  });
  rsbuild.connectWebSocket({ server });
}

startDevServer();

Modify startup script

After using a custom Server, you need to change the startup command from rsbuild dev to node ./server.mjs.

If you need to preview the online effect of SSR, you also need to modify the preview command. SSR Prod Server example reference: Example.

package.json
{
  "scripts": {
    "build": "rsbuild build",
    "dev": "node ./server.mjs",
    "preview": "node ./prod-server.mjs"
  }
}

Now, you can run the npm run dev command to start the dev server with SSR function, and visit http://localhost:3000/ to see that the SSR content has been rendered to the HTML page.

Get manifest

By default, scripts and links associated with the current page are automatically inserted into the HTML template. At this time, the compiled HTML template content can be obtained through getTransformedHtml.

When you need to dynamically generate HTML on the server side, you'll need to inject the URLs of JavaScript and CSS assets into the HTML. By configuring output.manifest, you can easily obtain the manifest information of these assets. Here's an example:

rsbuild.config.ts
export default {
  output: {
    manifest: true,
  },
};
server.ts
async function renderHtmlPage(): Promise<string> {
  const manifest = await fs.promises.readFile('./dist/manifest.json', 'utf-8');
  const { entries } = JSON.parse(manifest);

  const { js, css } = entries['index'].initial;

  const scriptTags = js
    .map((url) => `<script src="${url}" defer></script>`)
    .join('\n');
  const styleTags = css
    .map((file) => `<link rel="stylesheet" href="${file}">`)
    .join('\n');

  return `
    <!DOCTYPE html>
    <html>
      <head>
        ${scriptTags}
        ${styleTags}
      </head>
      <body>
        <div id="root"></div>
      </body>
    </html>`;
}

Examples