This website uses cookies

Our website, platform and/or any sub domains use cookies to understand how you use our services, and to improve both your experience and our marketing relevance.

Go hands-off. Autoscale your WordPress site on Cloudways Autonomous.Start Your Free Trial

Vite SSR in Production: Static vs Server-Rendered Deployment

Updated on August 7, 2026

13 Min Read
Dark code editor showing TypeScript imports and an async render function, with layered file panels and a teal stacked-layers icon on a blue gradient background.

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.

💡 Key Fact: In the e-commerce world, making a buyer wait three seconds means they are gone. You just lost a sale.

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.

Node.js official downloads page showing the Windows zip binary option

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

Terminal output after scaffolding a new Vite React project
Vite project creation success message with next step instructions

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

Terminal output after running npm install for project dependencies

npm install express

Terminal output confirming the Express package installation

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.

Vite SSR e-commerce project folder open in VS Code

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>

index.html file updated with app-html placeholder comment in VS Code

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>
  );
}

App.jsx code rendering a responsive product grid in VS Code

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:

Creating the entry-server.jsx file in VS Code for server-side rendering

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 };
}

entry-server.jsx code using renderToString to generate 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>
);

entry-client.jsx code hydrating the React app in the browser

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}`);
});

server.js Express code handling Vite SSR middleware and production routing

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.

package.json file showing the type module setting enabled

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.

E-commerce storefront running locally after starting the Express server
Page source showing product data rendered directly in the HTML

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"
}

package.json scripts configured for separate client and server builds

Now I just run npm run build.

Terminal output from running the dual Vite build command

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.

Terminal commands initializing Git before pushing the project 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.

Cloudways dashboard for launching a new Node.js hosting server

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.

Cloudways Starter plan selected for Node.js hosting
Cloudways server launch settings for the Node.js application

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

Connect Via Git screen linking a GitHub account in Cloudways
Selecting the vite-ssr-ecommerce GitHub repository in Cloudways

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.

Framework preset switched from Vite CSR to Vite SSR in Cloudways

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.

Cloudways build and output settings with npm and server.js entry file

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

Environment variables screen showing NODE_ENV set to production in Cloudways

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

npm install compression sirv

Terminal output after installing compression and sirv packages

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.

Cloudways deploy now progress pulling code from GitHub
Cloudways deployment logs confirming a successful Node.js build

Benchmarking Performance with Lighthouse

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

Live e-commerce storefront loaded from the deployed Cloudways URL

The page loads in a blink.

Browser showing the deployed Vite SSR site loading almost instantly

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.

Page source recording confirming product data is baked into the HTML

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

Lighthouse audit results showing strong performance scores

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.

Share your opinion in the comment section. COMMENT NOW

Share This Article

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.

×

Webinar: How to Get 100% Scores on Core Web Vitals

Join Joe Williams & Aleksandar Savkovic on 29th of March, 2021.

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Want to Experience the Cloudways Platform in Its Full Glory?

Take a FREE guided tour of Cloudways and see for yourself how easily you can manage your server & apps on the leading cloud-hosting platform.

Start my tour