Server-Side Rendering (SSR) is a technique in which HTML content is generated on the server, as opposed to client-side rendering where the browser generates content using JavaScript. SSR enhances performance, improves search engine optimization (SEO), and provides a faster time-to-interactive for web applications. This chapter will dive deep into advanced server-side rendering techniques with React, from basic setup to optimization, caching, hydration strategies, and integrating external data.
Server-Side Rendering (SSR) refers to the process of rendering HTML on the server rather than the client. This is commonly done in React applications to pre-render the HTML on the server and send it to the browser, where JavaScript then takes over (a process known as hydration).
In SSR, when a user requests a page, the server runs the JavaScript, executes the React code, and generates an HTML file. The browser receives this pre-rendered HTML content, ensuring faster initial page load times and better SEO performance.
The most common way to set up SSR with React is by using frameworks like Next.js, which abstracts much of the complexity. However, for understanding SSR in its entirety, we will first look at a basic implementation using Express and React.
1.Install the necessary packages:
npm install express react react-dom react-router-dom
2.Basic React component (App.js
):
import React from 'react';
const App = () => {
return (
Hello from Server-Side Rendering
);
};
export default App;
3.Server-Side Rendering with Express (server.js
):
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
const app = express();
app.get('*', (req, res) => {
const appString = renderToString( );
const html = `
SSR with React
${appString}
`;
res.send(html);
});
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
import React from 'react';
import { hydrate } from 'react-dom';
import App from './App';
hydrate( , document.getElementById('root'));
This basic setup involves rendering the HTML on the server and then hydrating it on the client. renderToString
is the method that turns the React component into an HTML string on the server.
Hydration is the process where React takes over the static HTML and attaches event listeners to the existing markup. Hydration comes with challenges, especially when there are differences between server-rendered HTML and client-side React.
Instead of hydrating the entire app at once, partial hydration allows only certain parts of the app to be hydrated. This technique can reduce load times by hydrating critical sections first.
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), );
Note: React 18 introduced hydrateRoot
, improving the efficiency of hydration. It allows for more control over when and how React rehydrates components, making it easier to implement advanced techniques like partial hydration.
Code splitting is a crucial optimization for SSR because you want to avoid loading all the JavaScript code for the entire application on the initial load.
Suspense
and React.lazy
with SSR:Normally, you can use React.lazy
for code splitting. However, SSR doesn’t support React.lazy
out-of-the-box. To implement code splitting on the server, you can use a package like loadable-components
.
loadable-components
:1.Install the package:
npm install @loadable/server @loadable/component
2.Modify Component with Lazy Loading (App.js
):
import loadable from '@loadable/component';
const LazyComponent = loadable(() => import('./LazyComponent'));
const App = () => (
My App
);
3.Update Server for SSR with Code Splitting: Use @loadable/server
to handle the server-side rendering of the lazy-loaded components.
import { ChunkExtractor } from '@loadable/server';
import path from 'path';
const statsFile = path.resolve('./dist/loadable-stats.json');
app.get('*', (req, res) => {
const extractor = new ChunkExtractor({ statsFile });
const appString = extractor.collectChunks( );
const html = `
${extractor.getLinkTags()}
${extractor.getStyleTags()}
${appString}
${extractor.getScriptTags()}
`;
res.send(html);
});
This way, code splitting is effectively integrated with SSR for improved performance.
Fetching data on the server is essential for improving performance and SEO. In React, data fetching can be done on the server before rendering and passed to the client.
app.get('*', async (req, res) => {
const data = await fetchDataFromAPI();
const appString = renderToString( );
const html = `
${appString}
`;
res.send(html);
});
In this example, data is fetched on the server, injected into the HTML, and then passed to the client for further use in the app.
Dynamic routing in SSR can be tricky since the routes need to be resolved on the server as well as the client.
react-router
:
import { StaticRouter } from 'react-router-dom/server';
app.get('*', (req, res) => {
const context = {};
const appString = renderToString(
);
if (context.url) {
res.redirect(301, context.url);
} else {
res.send(`
${appString}
`);
}
});
Here, StaticRouter
is used to handle routing on the server. This allows for correct rendering of routes on both the server and the client.
To improve the performance of SSR applications, you can implement caching on the server. Caching reduces the load on the server by storing the rendered HTML and serving it directly for subsequent requests.
const cache = new Map();
app.get('*', (req, res) => {
if (cache.has(req.url)) {
return res.send(cache.get(req.url));
}
const appString = renderToString( );
const html = `
${appString}
`;
cache.set(req.url, html); // Store the rendered HTML in cache
res.send(html);
});
In this example, an in-memory cache is implemented using a simple JavaScript Map
. When a request is made, the server first checks if the URL’s HTML is cached. If found, the cached HTML is served, reducing the need for re-rendering the page on each request.
For larger-scale applications, you can implement more sophisticated caching strategies using tools like Redis or CDN caching for even faster responses.
Error handling in SSR is crucial to ensure that even if something goes wrong during the server-side rendering process, the user still receives a meaningful response, rather than a blank screen or an error page.
app.get('*', (req, res) => {
try {
const appString = renderToString( );
res.send(`
${appString}
`);
} catch (error) {
console.error('SSR Error:', error);
res.status(500).send(`
Something went wrong on the server!
`);
}
});
In this example, errors during the rendering process are caught and logged, and a user-friendly error message is displayed instead of a blank page. In larger applications, logging errors can be extended using error monitoring services like Sentry or New Relic.
If SSR fails, the application should gracefully degrade to client-side rendering. This ensures the app remains functional, even if the server-side rendering pipeline fails.
app.get('*', (req, res) => {
let appString;
try {
appString = renderToString( );
} catch (error) {
console.error('SSR Failed, falling back to client-side rendering:', error);
appString = '';
}
res.send(`
${appString}
`);
});
Here, the server attempts to render the app, and if it fails, an empty string is sent instead of the pre-rendered HTML, allowing the client-side bundle to handle the rendering entirely.
When implementing SSR, there are several security concerns to address, such as cross-site scripting (XSS), data leakage, and ensuring that sensitive data is never exposed in the rendered HTML.
XSS occurs when malicious scripts are injected into your web page. A major vulnerability comes from rendering user-generated content on the server. You must always sanitize any user inputs or content that is rendered on the server.
Example with DOMPurify
to sanitize user input:
npm install dompurify
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const window = new JSDOM('').window;
const purify = DOMPurify(window);
app.get('*', (req, res) => {
const safeHtml = purify.sanitize('Hello
');
const appString = renderToString( );
res.send(`
${appString}
${safeHtml}
`);
});
In this example, DOMPurify
is used to sanitize any HTML to prevent malicious scripts from being injected into the server-rendered HTML.
Never send sensitive data, such as API keys, user passwords, or other secrets, in your rendered HTML. Always store such data in a secure place on the server and ensure that only necessary information is passed to the client.
Server-Side Rendering in React is a powerful technique that enhances performance, SEO, and overall user experience by rendering HTML on the server before sending it to the client. This chapter has walked through basic to advanced techniques of SSR, including hydration strategies, code splitting, caching, error handling, dynamic routing, and security considerations. Happy coding !❤️