Key Takeaways
- Client-side rendered Vite apps ship a blank page first, forcing users to wait through a download, parse, and API-fetch cycle before they see anything.
- Server-side rendering executes React on the backend so the browser receives a fully built HTML page instantly, with hydration happening quietly afterward.
- Serverless platforms introduce cold starts that can add one to three seconds of latency, which can cost e-commerce sites real sales.
- A persistent Node.js environment, like Cloudways Managed Node.js hosting, keeps the Express server always running, eliminating cold starts entirely.
Vite absolutely dominates local development right now. I use it for almost everything. But pushing your code live is a totally different story. You always hit the exact same roadblock. Do you deploy a standard client-side rendered app, or do you take the extra time to build out server-side rendering?
I am not going to bore you with the usual SEO lecture. We all know Google likes server-rendered HTML. I want to talk about raw speed instead.
My goal is simple. I want to drop the Time-to-First-Byte to zero. I want to kill off those annoying loading spinners that make users bounce.
To pull this off, I am going with a very specific stack. I am wiring up Vite and React to a custom Express server. After that, I will deploy the whole package onto a persistent Cloudways Node.js environment. No serverless functions. No cold starts. Just pure speed.
Why Vite SSR Needs a Dedicated Node Runtime
If you just run npm run build on a normal Vite React project, you get a static site. Standard static hosting works fine for that. But it brings a massive performance bottleneck.
The CSR Bottleneck
When a user visits a client-side rendered app, the server hands them a blank HTML file. It has a single <div id=”root”></div> tag and a link to a giant JavaScript bundle.
The browser has to download that blank page. Then it downloads the JavaScript. Then it parses the code. Then it finally runs it. And only then does the app realize it needs to fetch product data from an API. During this entire process, your user is staring at a blank white screen. It is a terrible experience.
The SSR Advantage
Server-side rendering flips the script completely.
When someone hits your URL, the server does the heavy lifting. It executes your React components right there on the backend. It fetches the product data. It builds the final HTML. Then it sends that fully cooked page down to the browser.
The user sees your storefront instantly. While they are looking at the products, React silently downloads the JavaScript in the background and “hydrates” the page to make buttons clickable.
The Hosting Reality
Here is the catch. You cannot achieve this on a basic static host. Standard static “Vite presets” on platforms like Netlify or GitHub Pages just serve flat files.
Vite actually has a highly optimized, low-level SSR API built right in. But it does not give you a production web server. You have to build that yourself. You need an active Express or Node.js process running 24/7 to catch incoming requests, run Vite’s rendering middleware, and spit out HTML strings dynamically.
Serverless Cold Starts vs. Persistent Node Runtimes
So you know you need a Node server. The next question is where you put it. The current trend is throwing everything onto serverless edge functions. I think that is a huge mistake for e-commerce.
The Cold Start Latency Penalty
Serverless platforms do not keep your app running all the time. They spin down your idle containers to save money.
When a new customer clicks your link, the platform wakes up. It boots a new container. It loads the Node environment. It parses your entire React tree. This wake-up process is called a “cold start.” It regularly adds one to three seconds of lag to the very first request.
Stop Losing Sales to Cold Starts
Get a persistent Node.js environment built for speed, with zero spin-up delay and a consistently low Time-to-First-Byte.
The Persistent Environment Advantage
I prefer using a persistent Node.js environment. This is exactly what Cloudways offers.
A persistent server keeps your Node process alive around the clock. There is no spin-up delay. When a request comes in, the event loop is already waiting for it. Bypassing that cold start gives you a consistently ultra-low Time-to-First-Byte.
You also get massive resource efficiency. Your server-rendered React builds will not crash into the strict memory limits or execution timeouts that plague serverless workers.
Mini Project: Build an E-Commerce Showcase with Vite SSR
I am going to build a fast, server-rendered product showcase to prove how this works. I will write a custom backend that fetches API data, hands it to a React component, and serves it as a complete HTML document.
Scaffolding the Vite and React SSR Workspace
First things first. I need to get my local workspace set up using Vite’s low-level SSR API.
I am actually writing this from my office laptop today. The IT department locked this machine down completely. I do not have admin rights to run standard installers. Because of this, I had to download the standalone binary .zip version of Node.js.

Command Prompt has no idea where Node lives on this computer. So before I can even scaffold the project, I have to tell CMD where my Node folder is manually.
I will open Command Prompt and run this exact command to update my local path:
set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.18.0-win-x64\node-v24.18.0-win-x64
It works perfectly. Now CMD knows how to execute Node and npm commands. I will change directories to my desktop and spin up the new Vite project.
cd C:\Users\abdulrehman\Desktop npm create vite@latest vite-ssr-ecommerce -- --template react


cd vite-ssr-ecommerce
Next up, I will install the dependencies. I need the standard React packages, plus Express to run the backend engine.
npm install

npm install express

I will open the folder in VS Code. To make SSR work, I have to tweak the root index.html file. It needs to act as a raw template for the Express server.

I will update index.html to include an injection placeholder:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite SSR E-Commerce</title>
</head>
<body>
<div id="root"><!--app-html--></div>
<script type="module" src="/src/entry-client.jsx"></script>
</body>
</html>

That <!–app-html–> comment is incredibly important. Later on, my Express server will hunt for that exact text and replace it with the dynamic HTML generated by React.
Fetching Product Data on the Server
An e-commerce demo is useless without actual products. I will use the free Fake Store API to grab product images, prices, and titles. I want all of this fetched on the server before a single line of HTML is generated.
Next, I’ll open src/App.jsx, clear out all the default Vite code. I don’t need it. I’ll replace it with my React component.
I am passing a products prop into the main app function. This allows the server to inject the data it just fetched.
import React from 'react';
export default function App({ products }) {
// Fall back to an empty array if products are undefined during early hydration
const productList = products || [];
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<header style={{ marginBottom: '40px', textAlign: 'center' }}>
<h1>E-Commerce SSR Showcase</h1>
<p>Ultra-fast server-rendered storefront</p>
</header>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '20px'
}}>
{productList.map(product => (
<div key={product.id} style={{
border: '1px solid #eaeaea',
padding: '20px',
borderRadius: '8px',
textAlign: 'center'
}}>
<img
src={product.image}
alt={product.title}
style={{ height: '150px', objectFit: 'contain', marginBottom: '15px' }}
/>
<h2 style={{ fontSize: '1.1rem', margin: '10px 0' }}>
{product.title.substring(0, 30)}...
</h2>
<p style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#2ecc71' }}>
${product.price}
</p>
<button style={{
background: '#000',
color: '#fff',
border: 'none',
padding: '10px 20px',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px'
}} onClick={() => alert(`Added ${product.title} to cart!`)}>
Add to Cart
</button>
</div>
))}
</div>
</div>
);
}

I kept the styling inline just to keep the project files clean. The key thing to notice here is the productList.map logic. It expects data immediately.
Configuring Dual Entries
React code running in a Node backend behaves very differently than React code running in Chrome. Node does not have a DOM. Chrome does. Because of this split environment, I need two separate entry files.
First, I will build the server entry. This file lives purely in Node space. It uses a specific React method called renderToString to create raw HTML text.
I will create a file named src/entry-server.jsx:

import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
export function render(url, data) {
const html = renderToString(
<React.StrictMode>
<App products={data} />
</React.StrictMode>
);
return { html };
}

Next up is the client entry. This one runs on the user’s browser. Its job is to grab the static HTML sent by the server and bring it to life. I will attach the initial API data to the global window object in my Express server, and this client file will pick it up.
I will create src/entry-client.jsx:
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// Pick up the data injected by the backend
const preloadedData = window.__PRELOADED_DATA__;
hydrateRoot(
document.getElementById('root'),
<React.StrictMode>
<App products={preloadedData} />
</React.StrictMode>
);

Building the Express Server Engine
Now for the fun part. I need to glue everything together.
I am building a local Express server. During development, it will run Vite in middleware mode to give me hot module replacement. In production, it will strip away the heavy Vite tools and serve lightweight, pre-compiled static files instead.
I will create server.js in the root of my project directory:
import fs from 'node:fs/promises';
import path from 'node:path';
import express from 'express';
import compression from 'compression';
import sirv from 'sirv';
const isProduction = process.env.NODE_ENV === 'production';
const port = process.env.PORT || 3000;
const base = process.env.BASE || '/';
const app = express();
let vite;
if (!isProduction) {
const { createServer } = await import('vite');
vite = await createServer({
server: {
middlewareMode: true,
allowedHosts: true
},
appType: 'custom',
base
});
app.use(vite.middlewares);
} else {
app.use(compression());
app.use(base, sirv(path.resolve(import.meta.dirname, 'dist/client'), { extensions: [] }));
}
async function fetchStoreData() {
const response = await fetch('https://fakestoreapi.com/products?limit=8');
return await response.json();
}
app.use('{*path}', async (req, res) => {
try {
const url = req.originalUrl.replace(base, '');
let template, render;
const productsData = await fetchStoreData();
if (!isProduction) {
template = await fs.readFile(path.resolve(import.meta.dirname, 'index.html'), 'utf-8');
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule('/src/entry-server.jsx')).render;
} else {
template = await fs.readFile(path.resolve(import.meta.dirname, 'dist/client/index.html'), 'utf-8');
const serverEntryPath = path.resolve(import.meta.dirname, 'dist/server/entry-server.js');
render = (await import(serverEntryPath)).render;
}
const rendered = await render(url, productsData);
const dataScript = `<script>window.__PRELOADED_DATA__ = ${JSON.stringify(productsData).replace(/</g, '\\u003c')}</script>`;
const html = template
.replace(`<!--app-html-->`, rendered.html ?? '')
.replace(`</head>`, `${dataScript}</head>`);
res.status(200).set({ 'Content-Type': 'text/html' }).send(html);
} catch (error) {
vite?.ssrFixStacktrace(error);
console.error(error.stack);
res.status(500).end(error.stack);
}
});
app.listen(port, '0.0.0.0', () => {
console.log(`Server listening on http://0.0.0.0:${port}`);
});

Because my backend script relies on top-level import statements, Node needs “type”: “module” enabled inside package.json to parse the files correctly. I can do a quick check to see if it’s present in my file. And yes…it is there.

With that requirement satisfied, my Express server is ready to launch.
I can test this setup locally right now by running node server.js in Command Prompt.
It works beautifully. The storefront appears immediately. If I right click and view the page source, all the product names and image links are sitting right there in the raw HTML. The server did its job.


Running Dual Production Builds
Vite’s middleware handles hot reloading, but it is way too slow for a live server. I need to bundle my app for production.
Since I have two entry points, I need two separate build outputs. One output goes to the client. The other output goes to the Node server.
I will update the “scripts” section in my package.json to handle this dual build process:
"scripts": {
"dev": "node server.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server",
"start": "NODE_ENV=production node server.js"
}

Now I just run npm run build.

Vite bundles all my React code and puts it into a dist/client folder. Then it takes my server entry file, compiles it for Node.js, and drops it into a dist/server folder. My Express script can now load these tiny compiled files instantly.
Before moving to the hosting stage, I will initialize Git and push my code up to GitHub.

git init git add . git commit -m "Initial Vite SSR build" git branch -M main git remote add origin https://github.com/abdulrehman293/vite-ssr-ecommerce git push -u origin main
Deploying to Cloudways Managed Node.js
My code is ready. Now I need a place to host it.

Deploy Your Own Vite SSR App Today
Launch a custom Express and Vite SSR project on a fully managed, always-on Node.js server in minutes.
I am deploying this on Cloudways Managed Node.js hosting. This is how I secure that persistent environment I mentioned earlier. It completely prevents cold starts.
I will open the Cloudways console and click Node.js on the left side menu. Then I will click Launch Now. I select the Starter plan. It packs more than enough power to run my little Express application.


On the next screen, I click Connect Via Git. I link my GitHub account and select my vite-ssr-ecommerce repository.


Now comes the most important deployment step. When I connected my repo, Cloudways auto-detected the project as Vite CSR and pre-filled standard defaults like Yarn for the package manager and dist/server/index.js for the entry file.
But since my setup uses a custom Express backend, I manually switched the framework preset to Vite SSR. This preset is a huge time-saver because Cloudways natively manages reverse proxy routing and PM2 process management right out of the box.

To align the Cloudways with my actual codebase, I opened the “Build and output settings” option and switched the package manager to npm, build command to npm run build, and pointed the entry file directly to server.js.

I also added NODE_ENV=production under Environment Variables so Express runs the compiled production build instead of Vite’s dev server.

And one last thing. I’ll install two production dependencies in my terminal:
npm install compression sirv

These packages let Express handle gzip compression and serve static assets out of ./dist/client once deployed.
Once installed, I committed the changes and pushed to GitHub:
git add . git commit -m "feat: complete Vite SSR setup for production" git push origin main
Finally, I hit Deploy Now. Cloudways takes over completely. It pulls my code from GitHub. It installs the packages. It runs my dual build script. Finally, it spins up the Node process and keeps it running permanently.


Benchmarking Performance with Lighthouse
The deployment takes a minute or two. Once it finishes, I click the live Cloudways URL to see the result.

The page loads in a blink.

I right click the page and hit View Page Source just to double check my work. The document is packed with data. Every single product title, image URL, and price is baked right into the DOM.

I ran a quick Lighthouse audit to get the actual numbers. The results are exactly what I wanted.

Because the Express server stays awake on Cloudways, there is absolutely zero cold start delay. The Time-to-First-Byte is incredibly low. The First Contentful Paint fires off instantly. The browser literally has everything it needs the moment the first network request completes.
Wrapping Up
Building a custom Vite SSR setup is absolutely worth the effort for data-heavy applications.
You stop forcing your user’s phone or laptop to do all the hard work. Your Express server fetches the API data and builds the HTML layout ahead of time. When you combine that architecture with a persistent host like Cloudways Velocity, you bypass the brutal latency penalties of serverless environments.
The end result is a lightning fast application that hooks your users from the very first second.
I’ve pushed the finished project to my GitHub, so feel free to clone it and reuse the code. And if you have any questions, let me know in the comments.
Q. Can Vite do SSR?
Yes. Vite is not a full stack framework like Next.js or Nuxt. However, it does provide a low level SSR API natively. You can use these built in tools to create a custom server rendered architecture backed by a Node.js and Express server.
Q. Which is better, SSR or CSR?
It really depends on what you are building. Client Side Rendering works great for private admin dashboards or internal tools where SEO does not matter at all. Server Side Rendering is the much better choice when you want search engine visibility, fast initial page loads, and great First Contentful Paint scores.
Q. Are developers still using Vite?
Yes. It is essentially the industry standard right now. Vite hit version 8.2.0 by mid 2026. It regularly pulls in over 160 million weekly downloads on npm. Vite 8 also introduced Rolldown, a very fast Rust based bundler. Major frameworks like SvelteKit, Astro, and Shopify Hydrogen all run on Vite.
Q. Can I do SSR with React?
Yes. You can write React SSR by picking a meta framework to handle the messy configuration for you. If you want more control, you can manually set up a Node server to run React’s renderToString method before sending the HTML response to the browser.
Abdul Rehman
Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.