DEV Community

Cover image for A Guide to Server-Side Rendering (SSR) with Vite and React.js
Arslan Ali
Arslan Ali

Posted on

A Guide to Server-Side Rendering (SSR) with Vite and React.js

Let’s dive deeper into the concept of server-side rendering (SSR) and how it can enhance the user experience of your web application.

The Concept of Server-Side Rendering

When a user visits your website, they typically receive bare HTML initially, which then triggers the loading of additional assets like JavaScript (e.g., App.js) and CSS (e.g., style.css). This traditional approach, often referred to as client-side rendering, means that the user must wait for these resources to download and execute before seeing any meaningful content. This delay can lead to a suboptimal user experience, especially for users on slow connections or devices.

Server-side rendering addresses this issue by sending the user a fully rendered HTML page in response to their initial request. This pre-rendered HTML includes the complete markup, allowing the user to see the content immediately without waiting for JavaScript to load and execute.

The key benefits of SSR include:

  • Reduced Time to Largest Contentful Paint (LCP): The user sees the content much faster because the server sends a complete HTML document.

  • Improved SEO: Search engines can index your content more effectively since the content is readily available in HTML.

  • Better Initial User Experience: Users can start reading and interacting with the content sooner, leading to higher engagement rates.

Balancing Performance Metrics

While SSR can reduce the LCP, it might increase the time of Interaction to Next Paint (INP). This is the time it takes for the user to interact with the page after it has loaded. The goal is to ensure that by the time the user decides to interact with the site, such as clicking a button, the necessary JavaScript has loaded in the background, making the interaction smooth and seamless.

A poor implementation of SSR can lead to a scenario where the user sees content but can't interact with it because the JavaScript hasn’t loaded yet. This can be more frustrating than waiting for the entire page to load initially. Therefore, it's crucial to continuously monitor and measure performance metrics to ensure that SSR is genuinely improving the user experience.

Setting Up SSR in Vite and React.js

We'll break this down into a few steps:

  1. Create a ClientApp Component
  2. Update index.html
  3. Create a ServerApp Component
  4. Set Up the Build Scripts
  5. Configure the Node Server

1. Create a ClientApp Component

We'll start by creating a ClientApp.jsx file, which will handle all the browser-specific functionality.

// ClientApp.jsx
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
Enter fullscreen mode Exit fullscreen mode

Here, we import hydrateRoot from react-dom/client, BrowserRouter from react-router-dom, and our main App component.

// ClientApp.jsx
// Hydrate the root element with our app
hydrateRoot(document.getElementById('root'), 
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
Enter fullscreen mode Exit fullscreen mode

We use hydrateRoot to render our app on the client side, specifying the root element and wrapping our App with BrowserRouter. This setup ensures all browser-specific code stays here.

Next, we need to modify our App.jsx.

// App.jsx
import React from 'react';

// Exporting the App component
export default function App() {
  return (
    <div>
      <h1>Welcome to My SSR React App!</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we keep our App component simple for demonstration purposes. We export it so it can be used in both client and server environments.

2. Update index.html

Next, we need to update index.html to load ClientApp.jsx instead of App.jsx and also add the parsing token to split the HTML file in the server, so we can stream the content in the root div.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="./vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"><!--not rendered--></div>
    <script type="module" src="./src/ClientApp.jsx"></script>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

3. Create a ServerApp Component

Now, let's create a ServerApp.jsx file to handle the server-side rendering logic.

// ServerApp.jsx
import { renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import App from './App';

// Export a function to render the app
export default function render(url, opts) {
  // Create a stream for server-side rendering
  const stream = renderToPipeableStream(
    <StaticRouter location={url}>
      <App />
    </StaticRouter>,
    opts
  );

  return stream;
}
Enter fullscreen mode Exit fullscreen mode

4. Set Up the Build Scripts

We'll need to update our build scripts in package.json to build both the client and server bundles.

{
  "scripts": {
    "build:client": "tsc vite build --outDir ../dist/client",
    "build:server": "tsc vite build --outDir ../dist/server --ssr ServerApp.jsx",
    "build": "npm run build:client && npm run build:server",
    "start": "node server.js"
  },
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

Here, we define separate build scripts for the client and server. The build:client script builds the client bundle, while the build:server script builds the server bundle using ServerApp.jsx. The build script runs both build steps, and the start script runs the server using server.js (which will be created in the next step).

Remove tsc from the client and server build if you are not using TypeScript.

5. Configure the Node Server

Finally, let's configure our Node server in server.js.

// server.js
import express from 'express';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import renderApp from './dist/server/ServerApp.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 3001;

// Read the built HTML file
const html = fs.readFileSync(path.resolve(__dirname, './dist/client/index.html')).toString();
const [head, tail] = html.split('<!--not rendered-->');

const app = express();

// Serve static assets
app.use('/assets', express.static(path.resolve(__dirname, './dist/client/assets')));

// Handle all other routes with server-side rendering
app.use((req, res) => {
  res.write(head);

  const stream = renderApp(req.url, {
    onShellReady() {
      stream.pipe(res);
    },
    onShellError(err) {
      console.error(err);
      res.status(500).send('Internal Server Error');
    },
    onAllReady() {
      res.write(tail);
      res.end();
    },
    onError(err) {
      console.error(err);
    }
  });
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});

Enter fullscreen mode Exit fullscreen mode

In this file, we set up an Express server to handle static assets and server-side rendering. We read the built index.html file and split it into head and tail parts. When a request is made, we immediately send the head part, then pipe the stream from renderApp to the response, and finally send the tail part once the stream is complete.

By following these steps, we enable server-side rendering in our React application, providing a faster and more responsive user experience. The client receives a fully rendered page initially, and the JavaScript loads in the background, making the app interactive.

Conclusion

By implementing server-side rendering (SSR) in our React application, we can significantly improve the initial load time and provide a better user experience. The steps involved include creating separate components for client and server rendering, updating our build scripts, and configuring an Express server to handle SSR. This setup ensures that users receive a fully rendered HTML page on the first request, while JavaScript loads in the background, making the application interactive seamlessly. This approach not only enhances the perceived performance but also provides a robust foundation for building performant and scalable React applications.

Top comments (0)