Modern React gives you two big server-side options: SSR (server‑side rendering) and RSC (React Server Components). They can deliver great UX, but they also add complexity, lock you to specific runtimes, and complicate deployments. For many apps, a simpler architecture wins: ship a fast SPA built with Vite, proxy API calls at the edge, and host it wherever you like—Nginx on Google Cloud or Vercel.
SSR and Server Components
SSR and Server Components: what problem they solve, and what they cost
-
What they solve
-
Faster first render for content-heavy pages (SSR streams HTML).
-
Better SEO for content that must appear in HTML immediately.
-
You can keep rendering code on the server and reduce client JS.
-
-
The trade-offs
-
Operational complexity: SSR/RSC require a server or “edge” runtime that runs your React tree per request. That’s more moving parts than serving static files.
-
Cost and scaling: You pay for CPU time on every request; you have to worry about cold starts, tuning concurrency, and cache strategies.
-
Framework coupling: Real-world SSR/RSC is framework-specific (e.g., Next.js). Your deployment model, bundling, routing, and data fetching become coupled to that framework and runtime assumptions.
-
Debugging and boundaries: Mixing server and client trees (RSC) introduces new failure modes, waterfalls if boundaries are off, and strict rules about what can run where.
-
Caching is harder: HTML is now dynamic, so you must design caching at multiple layers (per-route, per-segment, per-user) without serving the wrong thing.
-
For many dashboards, internal tools, and apps with authenticated traffic where SEO isn’t the driver, SSR often add more complexity than value.
How Vite serves React
Vite is a dev-and-build tool that makes React development instant and production builds tiny.
-
Dev server: On the fly ESM and lightning-fast HMR for React. You can proxy /api to any backend so you never fight CORS locally.
-
Production build: Uses Rollup to produce hashed, cacheable assets (JS/CSS) and an index.html shell. This is perfect for static hosting behind any CDN or proxy.
-
Optional SSR: Vite can power SSR if you really need it, but you don’t have to start there.
Simple Architecture
-
In the browser, your React app always calls /api/… .
-
In development, Vite proxies /api to your real backend. No CORS, no browser config.
-
In production, Vite hosted on Nginx (or Vercel rewrites) forwards /api to your backend. You keep a single origin, avoid CORS, and keep React completely backend-agnostic.
-
You get the easy scaling of static files plus the flexibility to point /api anywhere.
Hosting Options
-
Nginx that scales like a “function” on Google Cloud: Put Nginx in a small container, serve the Vite build as static files, and reverse-proxy /api to your backend. Deploy it on Cloud Run for autoscaling, scale-to-zero, and HTTPS without managing VMs.
-
Vercel static hosting: Let Vercel build or serve your Vite dist/ and add one rewrite rule that forwards /api to your backend. You get a global CDN and zero ops.
Minimal Code Example
React + Vite (client always calls /api)
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
function App() {
const [msg, setMsg] = useState("…");
useEffect(() => {
fetch("/api/health")
.then((r) => r.json())
.then((d) => setMsg(d.status || JSON.stringify(d)))
.catch((e) => setMsg(`Error: ${e.message}`));
}, []);
return (
<main style={{ fontFamily: "system-ui", padding: 24 }}>
<h1>Vite + React</h1>
<p>API health: {msg}</p>
</main>
);
}
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);Vite.config.ts (dev proxy for /api)
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true
// rewrite: (p) => p.replace(/^\/api/, "")
}
}
},
build: { outDir: "dist" }
});
Nginx config (Single Page Application + /api proxy)
Use react-router, tanstack-router etc. for client side routing…
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Serve static assets fast
location ~* \.(?:js|css|woff2?|ttf|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Reverse proxy for your API
location /api/ {
proxy_pass http://backend.internal:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# If your backend expects no /api prefix, add:
# rewrite ^/api/(.*)$ /$1 break;
}
}
Dockerfile for Cloud Run
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime: Nginx serves the built assets
FROM nginx:1.27-alpine
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/htmlMinimal vercel.json example if you want to deploy on vercel
{
"version": 2,
"rewrites": [{ "source": "/api/(.*)", "destination": "https://api.example.com/$1" }]
}
