Rendering Strategies: Every React Developer Should Know

Oğuz Kılıç
14 min readJun 5, 2024

--

Intro:

React is a key part of modern web development. To get the most out of it, you need to understand and use complex rendering strategies. This article will show you how.

These strategies help React developers create faster and more efficient apps. They make development easier and give users a better experience. These strategies can help you, no matter how much experience you have.

Understanding the Render Method

React’s rendering process is how it updates the UI efficiently. It has two main phases: render and commit.

Render Phase

The Render Phase is the first part of rendering where React builds a virtual UI. This phase doesn’t have side effects like DOM mutations or data fetching. It has two main steps:

  • Creating the Virtual DOM: When a component’s state or props change, React generates a new virtual DOM tree.
  • Diffing: React compares (or “diffs”) the new virtual DOM tree with the current fiber tree. This process determines what has changed in the new virtual DOM compared to the previous one.
  • Unit of Work: Fiber nodes represent units of work. Each fiber node corresponds to a React element (component or DOM element) and contains information about the component’s state, props, and other metadata.

Commit Phase

In this phase, the changes flagged during the render phase are applied to the actual DOM to reflect the latest UI state to the users.

  • Before Mutation Phase: Side effects that need to run before DOM mutations are handled here.
  • Mutation Phase: The actual DOM updates are applied based on the changes identified in the Fiber tree.
  • Layout Phase: Side effects that need to run after DOM mutations are handled in this phase.

The Commit Phase is synchronous and involves direct DOM manipulations, which can cause side effects.

Key Concepts:

  • Fiber Architecture: React’s fiber architecture lets you render in chunks. This makes React more efficient and handles large updates without blocking the main thread.
  • Concurrent Mode: React 18 introduced Concurrent Mode, which lets React work on multiple tasks at once. This makes the user experience smoother by pausing less important tasks and focusing on urgent updates.

What is Fiber?

Fiber is a data structure and algorithm introduced with React 16. It represents each node of the React component tree and makes the reconciliation process more flexible and efficient. The Fiber architecture is designed to improve performance, especially in large applications, and provide smoother user experiences.

Client-Side Rendering

Client-side rendering (CSR) is a web development technique where web pages are rendered on the client side, typically using JavaScript. This approach makes web pages more dynamic and interactive, but it also has challenges.

source: web.dev

Key Points:

1. Dynamic Content Loading: CSR allows content to be loaded dynamically without requiring a full page refresh.

2. Improved User Interaction: Since the rendering happens on the client side, interactions can be more responsive.

3. SEO Challenges: Search engines might have difficulty indexing CSR content as it relies heavily on JavaScript.

4. Initial Load Time: CSR can lead to slower initial load times because the browser must download and execute JavaScript before rendering the content.

Example:


import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

function App() {
const [data, setData] = useState(null);

useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));
}, []);

if (!data) {
return <div>Loading...</div>;
}

return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}

ReactDOM.render(<App />, document.getElementById('root'));

In this example:

  • The useEffect hook fetches data from an API when the component mounts.
  • The state (data) is updated with the fetched data, and the component re-renders to display it.
  • The initial content displayed is a loading message until the data is fetched.

Pros:

  • A rich user experience with interactive elements.
  • Efficient data fetching and rendering for single-page applications (SPAs)

Cons:

  • The first page takes a while to load.
  • SEO is hard without server-side rendering or pre-rendering.

Server-Side Rendering

Server-side rendering (SSR) is a technique where the server generates the HTML for a page and sends it to the client. This approach improves web app performance and SEO by delivering fully rendered pages directly from the server.

source: web.dev

Key Points:

1. Improved SEO: Since the content is fully rendered on the server, search engines can easily crawl and index the pages.

2. Faster Initial Load: Users receive a fully rendered page on the first request, which can make the initial load faster compared to CSR.

3. Dynamic Content: SSR can handle dynamic content, providing up-to-date information each time a page is requested.

Example:

const express = require('express');
const fetch = require('node-fetch');
const React = require('react');
const ReactDOMServer = require('react-dom/server');

const app = express();

app.get('/', async (req, res) => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();

const appString = ReactDOMServer.renderToString(<Home data={data} />);

const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server-Side Rendering with Express</title>
</head>
<body>
<div id="root">${appString}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(data)}
</script>
<script src="/client.js"></script>
</body>
</html>
`;

res.send(html);
});

app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});

How It Works:

  • Set Up the Server: The example uses the Express framework to set up a server. When a request is made to the root URL ('/'), it fetches data from an external API.
  • Fetch Data: The server makes an HTTP request to an API endpoint (https://api.example.com/data) using node-fetch. The fetched data is then converted to JSON format.
  • Render React Component: The server uses ReactDOMServer.renderToString to render the Home React component into an HTML string, passing the fetched data as a prop.
  • Send HTML Response: The server constructs an HTML template, embedding the rendered React component inside a div with an ID of root. It also includes a script to initialize the client-side data and load the client-side JavaScript file (client.js).
  • Client-Side Hydration: When the HTML is received by the browser, the client-side JavaScript (client.js) hydrates the server-rendered HTML, making the React components interactive.

Pros:

  • Better SEO with pre-rendered HTML.
  • Faster loading.
  • The page is always up to date.

Cons:

  • Each request takes longer to process.
  • Pages may take longer to load than with CSR.
  • It’s more complex to set up and maintain than static rendering.

Streaming Rendering

Streaming rendering is a technique where the server sends parts of the HTML to the client as soon as they are available. This improves the perceived performance of web applications by allowing the browser to start rendering the page.

source: web.dev

Key Points:

1. Improved Perceived Performance: Users can start interacting with parts of the page while the rest is still being rendered and streamed.

2. Progressive Rendering: Critical parts of the page are sent and rendered first, enhancing the user experience.

3. Reduced Time to First Byte (TTFB): Streaming can reduce the time to first byte, making the initial load feel faster.

Example:

React 18’s streaming rendering uses the new pipeToNodeWritable method for server-side rendering. This sends HTML parts to the client as soon as they’re ready.

const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { Writable } = require('stream');

const app = express();

function Home({ data }) {
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}

app.get('/', async (req, res) => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();

const htmlStart = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streaming Rendering</title>
</head>
<body>
<div id="root">
`;

const htmlEnd = `
</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(data)}
</script>
<script src="/client.js"></script>
</body>
</html>
`;

res.write(htmlStart);

const writable = new Writable({
write(chunk, encoding, callback) {
res.write(chunk, encoding, callback);
},
final(callback) {
res.write(htmlEnd);
res.end();
callback();
},
});

ReactDOMServer.pipeToNodeWritable(<Home data={data} />, writable);
});

app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});

How It Works:

  1. Server-Side: Uses ReactDOMServer.pipeToNodeWritable to stream the initial HTML.
  2. Client-Side: The client starts rendering parts of the HTML as they arrive.

Pros:

  • Users can interact with parts of the page as they load.
  • Critical content is shown first, improving the user experience.
  • The user interface is enhanced gradually as more content loads.
  • It uses less server and client resources because it doesn’t have to render the whole page at once.
  • Search engines can find content faster.

Cons:

  • Implementation is more complex than traditional rendering.
  • Manage dependencies and streaming order to ensure correct rendering.
  • Not all browsers or networks can stream.
  • Troubleshooting streaming issues is harder because it’s asynchronous.
  • If not managed carefully, there can be differences between server- and client-rendered content.

Static Site Generation

Static site generation is a technique where HTML pages are pre-rendered and served as static files. This approach improves performance, security, and reduces server load by serving pre-rendered HTML.

Key Points:

1. Performance: Since pages are pre-rendered and served as static files, the loading speed is very fast.

2. SEO: Pre-rendered HTML is easily crawled by search engines, improving SEO.

3. Security: Serving static files reduces the attack surface compared to server-rendered pages.

Example:

Here’s an example of SSG using Next.js.

// pages/index.js
import React from 'react';

function Home({ data }) {
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}

export async function getStaticProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();

return {
props: {
data,
},
};
}

export default Home;

In this example:

• The getStaticProps function fetches data at build time and runs on the server.

• The fetched data is passed as props to the Home component.

• The HTML is generated at build time and served as a static file.

Pros:

  • Fast load times for static files.
  • Improved SEO with pre-rendered HTML.
  • Faster servers and safer data.

Cons:

  • Less flexibility for changing content.
  • It needs to be built first.
  • Longer build times for large sites.

Incremental Static Regeneration

Incremental Static Regeneration (ISR) lets you update static pages after building and deploying a site. This approach combines the benefits of static site generation with the flexibility to update content.

Key Points:

1. On-Demand Updates: Pages can be updated incrementally as data changes, without needing a full site rebuild.

2. Improved Performance: Serves static content with the capability to refresh and update specific pages.

3. Flexibility: Combines the speed of static sites with the ability to handle dynamic content updates.

Example:

Here’s an example using Next.js to demonstrate ISR:

// pages/index.js
import React from 'react';

function Home({ data }) {
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}

export async function getStaticProps() {
const res = await fetch('https://api.example.com/data');
const data = await res.json();

return {
props: {
data,
},
revalidate: 10, // Regenerate the page at most once every 10 seconds
};
}

export default Home;

In this example:

  • The getStaticProps function fetches data at build time.
  • The revalidate property specifies the revalidation period (e.g., 10 seconds), allowing the page to be regenerated at most once in that period if requests come in.
  • The generated static HTML can be updated with new data without a complete site rebuild.

Pros:

  • It’s fast and flexible.
  • Improved SEO due to HTML.
  • It saves time and resources by reducing the need for frequent rebuilds.

Cons:

  • It’s more complex to set up than static site generation.
  • The latest content may not be visible to users immediately.

Rehydration

Rehydration in React involves rendering a React application on the server to generate the initial HTML, then having React take over on the client to make the page interactive. This process makes sure users get a fast start and can interact with the page when JavaScript is ready.

Key Points:

1. Server-Side Rendering (SSR): The server generates the initial HTML.

2. Client-Side Rehydration: The client-side React code takes over to make the page interactive.

Example:

Here’s a simple example demonstrating rehydration using React:

Server-Side

const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const fs = require('fs');
const path = require('path');

const App = require('./App').default;

const app = express();

app.use(express.static(path.resolve(__dirname, 'build')));

app.get('*', (req, res) => {
const appString = ReactDOMServer.renderToString(<App />);

const indexFile = path.resolve(__dirname, 'build', 'index.html');
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops!!');
}

return res.send(
data.replace('<div id="root"></div>', `<div id="root">${appString}</div>`)
);
});
});

app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});

Client-Side

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.hydrate(<App />, document.getElementById('root'));

How It Works:

1. Server-Side: The server uses ReactDOMServer.renderToString to generate HTML for the initial page load. This HTML is inserted into the index.html template.

2. Client-Side: When the client loads the page, ReactDOM.hydrate takes over the static HTML and attaches event listeners to make it interactive.

Pros:

  • Fast initial load times due to server-rendered HTML.
  • Improved SEO with pre-rendered content.
  • Full interactivity after the client-side JavaScript loads.

Cons:

  • Interactivity may be delayed until JavaScript is loaded.
  • It’s hard to make sure that server- and client-rendered content match.

Partial Hydration

Partial Hydration is a technique in web development where only parts of a static HTML page are made interactive. This approach improves performance by only loading and executing JavaScript for the parts of the page that require interactivity, reducing the overall JavaScript payload.

Key Points:

  • Selective interactivity: Only parts of the page are loaded.
  • Faster performance: It loads and executes less JavaScript, so pages load faster and use less resources.
  • Progressive enhancement: It makes sure the page works well and is easy to use.

Example:

Here’s an example demonstrating partial hydration using React:

Client-Side

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const rootElement = document.getElementById('root');
if (rootElement.hasChildNodes()) {
ReactDOM.hydrate(<App />, rootElement);
} else {
ReactDOM.render(<App />, rootElement);
}

React Components (App.js and InteractiveComponent.js)

// src/App.js
import React from 'react';
import InteractiveComponent from './InteractiveComponent';

function App() {
return (
<div>
<h1>Static Content</h1>
<InteractiveComponent />
</div>
);
}

export default App;
// src/InteractiveComponent.js
import React, { useState } from 'react';

function InteractiveComponent() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>Click me</button>
<p>{count}</p>
</div>
);
}

export default InteractiveComponent;

How It Works:

  1. Server-Side: The server uses ReactDOMServer.renderToString to generate HTML for the initial page load. This HTML is inserted into the index.html template.
  2. Client-Side: When the client loads the page, ReactDOM.hydrate selectively hydrates the parts of the page that need interactivity.

Pros:

  • Faster load times thanks to a smaller JavaScript payload.
  • Hydrates only the necessary parts of the page.
  • Users enjoy more interactivity.

Cons:

  • It’s more complex than full-page hydration.
  • It needs to be managed carefully.

Progressive Hydration

Progressive hydration is a technique where different parts of a web page are loaded one by one. This approach prioritizes the hydration of essential parts first, improving performance by delaying the hydration of non-essential parts.

Key Points:

  • Incremental hydration: Some parts of the page are gradually loaded.
  • Prioritized interactivity: Critical components are hydrated first.
  • Performance optimization: It reduces the initial JavaScript payload and improves page load times.

Example:

Here’s an example demonstrating progressive hydration using React and the Intersection Observer API:

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

function hydrateComponent(selector, Component) {
const element = document.querySelector(selector);
if (element && element.hasChildNodes()) {
ReactDOM.hydrate(<Component />, element);
} else if (element) {
ReactDOM.render(<Component />, element);
}
}

hydrateComponent('#root', App);
// src/App.js
import React, { useEffect } from 'react';

function App() {
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
import('./ProgressiveComponent').then(({ default: Component }) => {
hydrateComponent('#progressive', Component);
});
observer.disconnect();
}
});
});

observer.observe(document.querySelector('#progressive'));

return () => observer.disconnect();
}, []);

return (
<div>
<h1>Static Content</h1>
<div id="progressive">Loading...</div>
</div>
);
}

export default App;
// src/ProgressiveComponent.js
import React, { useState } from 'react';

function ProgressiveComponent() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>Click me</button>
<p>{count}</p>
</div>
);
}

export default ProgressiveComponent;

How It Works:

  1. Server-Side: The server uses ReactDOMServer.renderToString to generate initial HTML.
  2. Client-Side: ReactDOM.hydrate hydrates the main application immediately.
  3. Progressive Component Hydration: An Intersection Observer hydrates ProgressiveComponent when it enters the viewport.

Pros:

  • Hydrated only essential components first, improving load times.
  • Users have a better experience.
  • Fewer JavaScript files.

Cons:

  • It’s more complex than full-page hydration.
  • It needs to be managed carefully to avoid problems.

Dynamic Rendering

Dynamic rendering is a way of making web pages based on what users want. This approach optimizes websites for users and search engines by serving different content to crawlers and users.

Key Points:

1. Hybrid Approach: It optimizes performance and SEO by combining server-side and client-side rendering.

2. SEO Optimization: Serves pre-rendered HTML to search engines, improving crawlability and indexing.

3. User Experience: It lets users interact fully with the site.

Example:

Here’s an example of dynamic rendering using Node.js and Puppeteer:

const express = require('express');
const puppeteer = require('puppeteer');

const app = express();

app.get('*', async (req, res) => {
const userAgent = req.headers['user-agent'];

if (/Googlebot|Bingbot|Baiduspider|YandexBot/i.test(userAgent)) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(`http://localhost:3000${req.originalUrl}`, {
waitUntil: 'networkidle2'
});
const html = await page.content();
await browser.close();
res.send(html);
} else {
res.sendFile(__dirname + '/index.html');
}
});

app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
// src/App.js
import React, { useState, useEffect } from 'react';

function App() {
const [data, setData] = useState(null);

useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);

if (!data) {
return <div>Loading...</div>;
}

return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}

export default App;

API Endpoint (Node.js + Express)

const express = require('express');
const app = express();

app.get('/api/data', (req, res) => {
res.json({
title: 'Dynamic Rendering Example',
content: 'This content is fetched from the server.'
});
});

app.listen(3001, () => {
console.log('API server is running on http://localhost:3001');
});

In this example:

  • The server checks the user-agent header to see if the request is from a search engine bot.
  • If the request is from a bot, Puppeteer generates and returns pre-rendered HTML.
  • If the request is from a user, the client-rendered HTML is served, and the React application fetches data from an API endpoint.

Pros:

  • It combines server-side and client-side rendering.
  • Serves pre-rendered content to search engine bots, which optimizes SEO.
  • Users can interact with it.

Cons:

  • Server setup and maintenance are more complex.
  • It uses more resources because it needs a headless browser for pre-rendering.
  • Handle user-agent detection and dynamic content generation carefully.

Conclusion

In the fast-changing world of web development, it’s important to understand and use different ways of making web pages look good and load quickly. Each rendering method has different benefits and drawbacks.

Key Takeaways:

1. Client-Side Rendering (CSR) is great for users but can be tricky for SEO and loading times.

2. Server-Side Rendering (SSR) ensures faster initial loads and better SEO but can increase server load and complexity.

3. Streaming Rendering enhances perceived performance by progressively sending HTML to the client.

4. Static Site Generation (SSG) delivers lightning-fast load times and enhanced security, ideal for content that doesn’t change frequently.

5. Incremental Static Regeneration (ISR) combines the benefits of static and dynamic content updates, offering a balance of performance and flexibility.

6. Rehydration bridges server-rendered HTML and client-side interactivity, providing a smooth user experience.

7. Partial Hydration and Progressive Hydration optimize performance by selectively making parts of the page interactive as needed.

9. Dynamic Rendering serves pre-rendered content to bots for better SEO while providing dynamic content to users for an enhanced experience.

The right rendering strategy depends on your app’s needs, performance, and user experience. By using these techniques, developers can create user-friendly web apps that work well for users and search engines.

As web technologies improve, developers must stay up to date with the best ways to make web apps run fast. Use these methods to create great digital experiences.

--

--

Oğuz Kılıç

developer @trendyol, ex @eBay, sci-fi addict. JavaScript, Frontend, Software Architecture