Introduction
Server-Side Rendering (SSR) is a technique where your React application is rendered on the server before being sent to the browser.
This improves performance, SEO, and user experience. In this guide, we’ll set up SSR using Express and Compression with React and Vite.
Why Use SSR with Express?
By default, React is a client-side framework, meaning pages are rendered in the browser. However, for SEO and performance,
SSR pre-renders the content on the server and delivers a ready-to-view HTML page. Express helps us serve this efficiently,
while Compression reduces payload size for faster responses.
⚠️ Note: This setup is for React with Vite, but the same pattern works with other frameworks using Express as the server.
Install Dependencies
Run the following command to install necessary packages:
npm i compression express cross-env
Project Structure
Your project should have the following important files:
server.js
→ Handles Express server and SSR logic
index.html
→ HTML template
entry-client.jsx
→ Hydrates React on client
entry-server.jsx
→ Renders React on server
Server Configuration (server.js)
Below is the Express server setup with Vite middleware for development and Compression for production:
import fs from 'node:fs/promises'
import express from 'express'
// 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')
: ''
// 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('*all', async (req, res) => {
try {
const url = req.originalUrl.replace(base, '')
let template
let render
if (!isProduction) {
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)
const html = template
.replace(``, rendered.head ?? '')
.replace(``, 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}`)
})
HTML Template (index.html)
<!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</title>
<!--app-head-->
</head>
<body>
<div id="root"><!--app-html--></div>
<script type="module" src="/src/entry-client.jsx"></script>
</body>
</html>
Client Entry (entry-client.jsx)
import './index.css'
import { StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import App from './App'
hydrateRoot(
document.getElementById('root'),
<StrictMode>
<App />
</StrictMode>,
)
Server Entry (entry-server.jsx)
import { StrictMode } from 'react'
import { renderToString } from 'react-dom/server'
import App from './App'
export function render(_url) {
const html = renderToString(
<StrictMode>
<App />
</StrictMode>,
)
return { html }
}
Run the Application
Use the following command to run your SSR app in development mode:
Conclusion
With this setup, your React application now supports server-side rendering with Express and Compression.
You’ll see performance improvements, SEO benefits, and a smoother user experience.