🔥 ⚔️ Rendering Wars: Unveiling the Differences - CSR vs. SSR in ReactJS

2023-03-07

Perhaps one of the most important and fundamental decisions is how to render our web application when we start architecting a new web app. That’s why today I want to talk about two ways of rendering, client-side rendering and server-side rendering.Of course, these two patterns are not the only ones, and we will discuss progressive rendering and other patters in the near future.

Terminology I will use 💬

🎨 Rendering

⚡️ Performance

Let’s start with CSR. In such a rendering process, we have almost the empty html, and the the builded Javascript. We are generating and building the Dom by loading this JS code into the browser.


CSR

Structure ⛩️ 🏗️:

Consider we have HTML with an empty div tag and bundled js on the server. CSR rendering looks like this:

animation

The HTML consists of just a single root div tag and all the other things like displaying and updating the DOM are handled by JS.

🚀 Bundles and Performance

If we think about the performance, we first see that as the complexity of the page increase, the complexity and size of JS will increase as well and the time to get it from the server will increase. That will cause a delay for the FCP and TTI of the page.

CSR Renreding Performance

⚖️ Pros and Cons

Pros: ✅

Cons: ❌

There are a few pitfalls though:

Crawlers indexing

Since performance issues are caused by the Javascript bundle size there are some approaches that improve the performance of CSR.

Service workers

Server-Side Rendering

Let’s take a look what is SSR. It’s the oldest method to generate the full HTML for the page content to be rendered during the user makes a request. For this implementation we need to build the html on the server side and send it to the browser so the browser can easily build a FCP.

ssr-gif

The browser makes a request on the server and gets the HTML not just empty root but also generated with the content in it. So the browser shows the content as soon as it’s possible. Now the browser starts downloading your bundle, and the user still sees the content on the screen but it’s not interactive yet. Only after your bundle is downloaded, it attach event listeners to your HTML and becomes interactive. This process we call hydration or rehydration.

⚖️ Pros and Cons

Pros: ✅

Less Javascript enables the browser for quicker FCP and TTI.

SSR Performace

Since we are avoiding sending lots of Javascript to the Client FCP and users are just waiting for TTI. When we have lots of UI elements on the page SSR has way less JS than CSR so the time to get the scripts is lesser even if sometimes we have FP=FCP=TTI.

Cons: ❌

Sometimes we may have a slow TTFB. That would be a scenario when the user has a Slow network or the Server code is not optimized.

Since all our code is not available on the client side then the frequent calls to the server cause full page reloads and sometimes this will increase the time between interactions. Thus SPA is not possible with SSR.


Let’s create simple React SSR app with Express.


Step: 1

Create React application

bash
npx create-react-app ssr-react

Step: 2

Remove extra files in src folder and just left App.js and index.js.

Step: 3

Let’s write component : counter with increase and decrease methods.

src/App.js
 
import React, { useState } from 'react';
 
const App = ({ initialValue }) => {
	const [count, setCount] = useState(initialValue);
	if (typeof window === 'undefined') return <></>;
	return (
	  <>
		<div className="App">
			<button onClick={() => setCount(count + 1)}>Increase</button>
			<div style={{ margin: "20px" }}>{count}</div>
			<button onClick={() => setCount(count - 1)}>Decrease</button>
		</div>
	  </>
	);
};
 
export default App;
 

Step: 4

Now we want to render it server side. First we need a server so create a new folder server and add the Server.js file into it. I will use express to create a server, let’s install it first.

bash
npm i express --save

Create a express server and listen it on port 3000.

server/server.js
import express from 'express';
const app = express();
 
app.listen(3000, function () {
  console.log('server running on 3000');
});

Step: 5

No we have a server. What we need is to get the builded html file. We will render the App component and pass it to the response. We need to serve out statice files as well.

server/server.js
import express from 'express';
import fs from 'fs';
import path from 'path';
import App from '../src/App';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
const app = express();
 
app.get("/page", async (req, res) => {
  fs.readFile(path.resolve("./build/index.html"), "utf-8", (err, data) => {
	
	if (err) {
	  return res.status(500).send("Error happened");
	}
 
	const html = `<div id="root">${ReactDOMServer.renderToString(
		<App />
	)}</div>`;
 
	return res.send(data.replace('<div id="root"></div>', html));
  });
});
 
app.use(express.static(path.resolve(\_\_dirname, "..", "build")));
 
app.listen(3000, function () {
console.log("server running on 3000");
});
 

Step:6

So we read index.html file from the built folder. Then we need to render the App component on the server side, for this, we use ReactDOMServer which provides the method to render the component on the server side. Replace the empty div root with our rendered component da send it to the client. So we are using JSX on the server side and need support for it, so we need to add Babel for it.

npm i @babel/preset-env @babel/preset-react @babel/register ignore-styles --save-dev

Step: 7

Add index.js file side by server.js and require these things into it and require server.js on bottom of them.

server/index.js
require("ignore-styles");
 
require("@babel/register")({
    ignore: [/(node_module)/],
    presets: ["@babel/preset-env", "@babel/preset-react"],
});
 
require("./server");
 

Step: 8

On the frontend side, we don’t need ReactDOM.render anymore since we have already rendered the DOM from the server and now we need to hydrate it to register event listeners and etc. Now index.js on the src folder looks like this.

src/index.js
import React, { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";
 
hydrateRoot(document.getElementById("root"), <App initialValue={10} />);

Step: 9

Add the final run command in package.json file:

    "ssr": "node server/index.js",

The first build our frontend by the command npm run build and then start our server by the command npm run ssr.

Source Code

Thanks for your attention 🚀🖌️!