Let’s dive deeper into the concept of server-side rendering (SSR) and how it can enhance the user experience of your web application.
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.
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.
We'll break this down into a few steps:
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';
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> );
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> ); }
Here, we keep our App component simple for demonstration purposes. We export it so it can be used in both client and server environments.
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>
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; }
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" }
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.
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}`); });
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.
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.
The above is the detailed content of A Guide to Server-Side Rendering (SSR) with Vite and React.js. For more information, please follow other related articles on the PHP Chinese website!