Open In App

Ultimate Guide to Server-Side Rendering (SSR) with Vite and ReactJS

Last Updated : 23 Sep, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

Server-side rendering (this practice allows making web pages since the browser only enables blank pages) can be defined as one of the current trends in web development. SSR offers many advantages – performance improvement, SEO enhancement, and even convenience for users. As opposed to client-side rendering only after the complete page is loaded, which can disappoint users with slower devices or networks, SSR allows for steaming pages during loading which is great on the large part of the slow pages.

Server-side-rendering

Below are the points to discuss in the article:

What is SSR?

Server-side rendering (SSR) is a web application technique in which the HTML of web pages is generated on the server and then sent to the client. This method allows for faster initial page loads because the server prepares the content, reducing the time the browser spends processing JavaScript to render the page.

Why SSR Matters?

SSR significantly improves the performance and accessibility of web applications by pre-rendering the HTML on the server. This leads to faster load times and enhanced user experiences, particularly on slower devices or networks. Additionally, SSR improves the ability of search engines to index content, as the fully rendered HTML is readily available for crawling.

Benefits of SSR in Modern Web Applications

Alright. Instead, let’s see what is so special about its implementation that ought to come last.

  • Increased Speed: The main reason why content is thought to be able to deploy SSR, is that users are served content more quickly through this method as the page has been prepared at the server several cuts down TTFB as well as perceived load time. That is especially useful for users on a slow connection or running low on power.
  • SEO Optimization: Search engines have an easier time crawling and indexing HTML content rendered on the server. SSR improves visibility on search engines compared to client-rendered JavaScript content.
  • Enhanced Indexing: The crawlers or bots will find it easier to index the content that has been pre-rendered in the server in the structure of an HTML document. SSR enhances the reachability of the content than client-only rendered JavaScript content.
  • Better Accessibility: Content rendered on the server is more accessible to assistive technologies, which improves usability for users relying on screen readers or other aids.

Challenges and Considerations

  • Complexity: SSR requires more complex server configurations and an understanding of both server and client environments.
  • Performance Overhead: Rendering on the server can add load, making efficient caching strategies critical.
  • Hydration: Ensuring client-side JavaScript takes over the static HTML without issues can be tricky.

Implementing SSR in React with Vite

Vite is an app-building tool that provides fast build and development modes. It directly utilizes the ES modules of the browser for development and then uses Rollup to bundle the code for Production. From Vite’s side, it is reasonable to use it with frameworks, like React because of the HMR HOT replacement efficiency and processing speed.

On the other hand, React.js is a component-based library for building user interfaces. React allows developers to build encapsulated components that manage their state and then compose them to make complex user interfaces more efficient through changes in data.

Together, Vite and React provide the best combination for building performant and scalable web applications.

Setting up a React Project with Vite and SSR

Let’s start by setting up a new Vite project configured for React:

Step 1: Install Vite

First, create a new Vite project by running the following commands in your terminal:

npm create vite@latest

Step 2: Then Follow the below instructions

Choose the following options:

Other -> create-vite-extra -> ssr-react -> non-streaming -> javascript/typescript

Project Structure:

Screenshot-2024-09-08-230853
Project structure

Updated Dependencies:

  "dependencies": {
"compression": "^1.7.4",
"express": "^4.19.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sirv": "^2.0.4"
},
"devDependencies": {
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@vitejs/plugin-react": "^4.2.1",
"cross-env": "^7.0.3",
"vite": "^5.2.10"
}
  • Now that you have the package.json and list of dependencies, let's walk through the entire setup process, including configuring Vite for SSR and creating the basic server file.
  • To install all the necessary dependencies, run the following command in your project directory:
npm install

Approach to Create SSR Application

Vite requires some configuration changes to support SSR. We will need to set up a server entry point and modify the Vite configuration file.

Step 1: Create a Server Entry File

Started by creating a new file named server.js in the root of your project. This file will handle server-side rendering and serve your React application.

To implement SSR effectively, you need to set up the server to render your React components and manage routing correctly.

  • Server-Side Rendering: Use renderToString from react-dom/server to render your React components on the server side.
  • Serve the HTML: In the server response, send the fully rendered HTML along with a script tag that points to your client-side JavaScript entry point.
JavaScript
import express from "express"
import fs from "node:fs/promises"

// Constants
const isProduction = process.env.NODE_ENV === "production"
const port = process.env.PORT || 5173
const base = process.env.BASE || "/"

// Cached production assets
const templateHtml
    = isProduction ? await fs.readFile(
          "./dist/client/index.html", "utf-8")
                   : ""
const ssrManifest
    = isProduction ? await fs.readFile(
          "./dist/client/.vite/ssr-manifest.json", "utf-8")
                   : undefined

// Create http server
const app = express()

// Add Vite or respective production middlewares
let vite
if (!isProduction)
{
    const {createServer} = await import("vite")
    vite = await createServer({
        server : {middlewareMode : true},
        appType : "custom",
        base
    })
    app.use(vite.middlewares)
}
else {
    const compression
    = (await import("compression")).default const sirv
    = (await import("sirv")).default app.use(compression())
  app.use(base, sirv("./dist/client", { extensions: [] }))
}

// Serve HTML
app.use("*", async (req, res) => {
    try {
        const url = req.originalUrl.replace(base, "")

        let template let render
        if (!isProduction)
        {
            // Always read fresh template in development
            template = await fs.readFile("./index.html",
                                         "utf-8") template
                = await vite.transformIndexHtml(url,
                                                template)
            render = (await vite.ssrLoadModule(
                          "/src/entry-server.jsx"))
                         .render
        }
        else
        {
            template = templateHtml
            render = (await import(
                          "./dist/server/entry-server.js"))
                         .render
        }

        const rendered = await render(url, ssrManifest)

    const html = template
      .replace(`<!--app-head-->`, rendered.head ?? "")
      .replace(`<!--app-html-->`, rendered.html ?? "")

    res.status(200)
        .set({"Content-Type" : "text/html"})
        .send(html)
    }
    catch (e) {
        vite?.ssrFixStacktrace(e)
        console.log(e.stack)
        res.status(500).end(e.stack)
    }
})

  // Start http server
  app.listen(
      port,
      () => {console.log(
          `Server started at http://localhost:${port}`)})

This basic Express server renders your React application using renderToString and serves the generated HTML to the client.

Step 2: src/entry-client.jsx: Set up client-side hydration

This code will sets up the entry point for a React application with server-side rendering support. It imports the necessary CSS and React libraries, then uses ReactDOM.hydrateRoot to attach the React app to an existing HTML element that was pre-rendered on the server. Wrapping the app in React.StrictMode helps identify potential issues during development by providing additional checks and warnings. The BrowserRouter component from react-router-dom enables client-side routing, allowing seamless navigation between different views without full page reloads. Inside the BrowserRouter, the Router component (imported from the local ./router module) manages the application's routes and navigation logic, ensuring a smooth and interactive user experience.

JavaScript
import "./index.css"
import React from "react"
import ReactDOM from "react-dom/client"
import {BrowserRouter} from "react-router-dom"
import {Router} from "./router"

ReactDOM.hydrateRoot(document.getElementById("root"),
                     <React.StrictMode><BrowserRouter>
                     <Router />
                     </BrowserRouter>
</React.StrictMode>)

Step 3: src/entry-server.jsx: Set up server-side rendering

This code snippet will sets up server-side rendering for a React application. It imports necessary modules from React and React Router, specifically ReactDOMServer for rendering React components to a static HTML string and StaticRouter for handling routing on the server side.

The render function uses ReactDOMServer.renderToString to convert the React component tree into an HTML string. The StaticRouter component, which is part of react-router-dom/server, manages the routing based on the server-side location prop (though path should be defined or passed in a real scenario). The React.StrictMode wrapper is used for development checks. Finally, the function returns an object containing the generated HTML, which can be sent to the client to be rendered in the browser.

JavaScript
import React from "react"
import ReactDOMServer from "react-dom/server"
import {StaticRouter} from "react-router-dom/server";
import {Router} from "./router"

export function render()
{
    const html = ReactDOMServer.renderToString(
        <React.StrictMode><StaticRouter location = {path}>
        <Router /></StaticRouter>
    </React.StrictMode>)
    return { html }
}

Step 4: Build and Run the Application

Build the Application:

npm run build
npm run build:client
npm run build:server

This command builds the client and server parts of the application.

Run the application:

npm run dev

Your server will start at http://localhost:5173, serving the SSR React application.

Screenshot-2024-09-08-233318
output

Routing with SSR

With SSR, routing must be handled on both the server and the client. On the server, use StaticRouter from react-router-dom/server to render the correct routes based on the incoming request URL. On the client side, use BrowserRouter to handle navigation without a full page reload.

Step 1: Create a router.jsx file inside the src folder of the application

This code defines a simple routing setup for a React application using react-router-dom. It imports the Route and Routes components to configure routing, and it also imports the Contact and Home components from the views directory.

The Router component sets up the routing configuration by defining a set of routes within the Routes component. It maps the root path ("/") to the Home component and the /contact path to the Contact component. This setup allows users to navigate between the home page and the contact page within the application.

JavaScript
import {Route, Routes} from "react-router-dom";

import {Contact, Home} from "../views";

export const Router = () => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  );
};

Step 2: Update the entry-client.jsx

JavaScript
import "./index.css"

import React from "react"
import ReactDOM from "react-dom/client"
import {BrowserRouter} from "react-router-dom"

import {Router} from "./router"

ReactDOM.hydrateRoot(document.getElementById("root"),
                     <React.StrictMode><BrowserRouter>
                     <Router />
                     </BrowserRouter>
  </React.StrictMode>)

Step 3: Update the entry-server.jsx

JavaScript
import React from "react"
import ReactDOMServer from "react-dom/server"
import {StaticRouter} from "react-router-dom/server";
import {Router} from "./router"

export function render()
{
    const html = ReactDOMServer.renderToString(
        <React.StrictMode><StaticRouter location = {path}>
        <Router /></StaticRouter>
    </React.StrictMode>)
    return { html }
}

Data Fetching Strategies

  • Handling data fetching in SSR can be complex but crucial for performance and user experience. You can use libraries like react-query or implement custom hooks that support server-side data fetching.
  • Fetch data before rendering the component on the server. Pass the fetched data as props to your components.
  • Ensure that the data fetched on the server is available to the client during hydration to prevent duplication of requests.

Handling State Management

Integrate state management solutions like Redux, Zustand, or Context API to manage application state across server and client. Ensure the initial state is serialized and included in the HTML sent from the server.

Optimizing Performance

  • Code Splitting: This can be achieved in the following ways – by utilizing lazy loads provided by the react frameworks and dynamic imports in Vite.
  • Caching: Aim of this strategy is to reduce the load on the server and improve its performance by creating caches of rendered HTML pages and other static files.
  • Minification and Compression: This performs several elementary operations such as: javascript minification with the usage of terser, and gzip or brotli as a response pack.

Next Article

Similar Reads