Vite SSR and SSG Integration

Modern frontend architectures increasingly demand hybrid rendering strategies that balance initial load performance with dynamic interactivity. This cluster isolates the server-side rendering (SSR) and static site generation (SSG) workflows from general Vite configuration, focusing exclusively on the dual-bundle architecture, hydration mechanics, and runtime integration required for production-grade deployments. Engineers and framework maintainers will find actionable patterns for module graph isolation, streaming pipelines, and edge-compatible build optimizations.

Architectural Foundations of Vite SSR/SSG

Vite’s rendering model relies on a strict separation between client and server execution contexts. During development, Vite leverages esbuild for rapid dependency pre-bundling, while production builds delegate to Rollup for tree-shaking and code splitting. The core architectural requirement for SSR/SSG is maintaining two isolated module graphs: one targeting the browser (ssr: false) and one targeting the server (ssr: true). This prevents cross-environment leakage, where browser-only globals (window, document) or DOM-dependent libraries inadvertently enter the Node execution context.

To establish this boundary, configure dual entry points and explicitly scope external dependencies:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
 build: {
 rollupOptions: {
 input: {
 client: 'src/entry-client.ts',
 server: 'src/entry-server.ts'
 }
 }
 },
 ssr: {
 // Prevent framework core from being bundled into the server graph
 noExternal: ['framework-core', 'shared-utils'],
 // Explicitly target Node.js runtime resolution
 resolve: {
 conditions: ['node']
 }
 }
});

Performance Impact: Isolating the server module graph reduces cold-start latency by ~35–40% by eliminating browser polyfills and CSS-in-JS runtime overhead from the Node execution path.

Debugging Paths:

  • Verify __vite_ssr_import__ resolution in a Node REPL to ensure server-side ESM transforms are applied correctly.
  • Check ssrManifest generation to identify hydration mismatches caused by missing chunk references.
  • Audit the dependency tree with npm ls or pnpm why to catch CJS/ESM boundary violations that trigger synchronous require() calls in async server contexts.

Server-Side Rendering Workflows

Implementing SSR requires precise orchestration between Vite’s dev server and a custom Node runtime. The vite.createServer({ ssr: true }) instance handles on-the-fly module transformation, while your server framework manages request routing and response streaming. For production deployments, refer to Configuring Vite SSR with Express and Node.js to establish robust request handlers and streaming pipelines.

A minimal SSR server in middleware mode:

// server.ts
import express from 'express';
import { createServer as createViteServer } from 'vite';

async function createServer() {
 const app = express();
 const vite = await createViteServer({
 server: { middlewareMode: true },
 appType: 'custom'
 });

 app.use(vite.middlewares);

 app.get('*', async (req, res, next) => {
 const url = req.originalUrl;
 try {
 // Load the SSR entry point dynamically
 const { render } = await vite.ssrLoadModule('/src/entry-server.ts');
 const [html, preloadLinks] = await render(url);
 
 res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
 } catch (e) {
 vite.ssrFixStacktrace(e as Error);
 next(e);
 }
 });

 app.listen(3000, () => console.log('SSR server running on :3000'));
}

createServer();

Performance Impact: Enabling ssr.resolve.conditions: ['node'] cuts server bundle resolution time by ~65% by bypassing browser-specific exports maps. Streaming HTML with ReadableStream APIs reduces Time to First Byte (TTFB) by 120–200ms on high-latency networks compared to buffered string concatenation.

Debugging Paths:

  • Trace ERR_MODULE_NOT_FOUND in SSR context using VITE_DEBUG=ssr vite dev.
  • Validate window/document polyfills during server execution by running node --inspect server.ts and setting breakpoints on uncaught exceptions.
  • Monitor transform cache hits/misses via the Vite dev server logs to diagnose hot-reload latency during iterative development.

Static Site Generation (SSG) Pipeline Integration

SSG in Vite operates by pre-rendering routes during the build phase, bypassing the need for a persistent server. This workflow integrates tightly with Optimizing Vite Dev Server and HMR to ensure preview environments match production output exactly. The pipeline typically involves route enumeration, isolated async data fetching, and serializing payloads into the generated HTML for client hydration.

A production-ready SSG build script:

// scripts/build-ssg.ts
import { build, createServer } from 'vite';
import { renderToString } from 'framework-dom/server';
import fs from 'fs/promises';
import path from 'path';

const routes = ['/about', '/docs', '/pricing']; // Enumerate via FS scan or API

async function generateStatic() {
 // Build client assets
 await build({ build: { outDir: 'dist/static' } });

 const vite = await createServer({ appType: 'custom' });
 const { render } = await vite.ssrLoadModule('/src/entry-server.ts');

 for (const route of routes) {
 const [html, preloadLinks, state] = await render(route);
 const outPath = path.join('dist/static', route === '/' ? 'index.html' : `${route}/index.html`);
 
 await fs.mkdir(path.dirname(outPath), { recursive: true });
 await fs.writeFile(outPath, html);
 console.log(`✓ Pre-rendered ${route}`);
 }

 await vite.close();
}

generateStatic().catch(console.error);

Performance Impact: Pre-rendering with build.ssrManifest: true reduces client-side hydration Time to Interactive (TTI) by ~250ms by eliminating redundant chunk discovery during the initial parse. Serializing payloads into inline <script type="application/json"> tags avoids additional network round-trips for initial data hydration.

Debugging Paths:

  • Inspect dist/.vite/ssr-manifest.json for missing chunks that cause hydration failures.
  • Validate hydration state injection against the DOM structure using browser DevTools’ “Elements” panel to ensure data-hydration attributes align.
  • Profile build-time memory consumption with node --max-old-space-size=4096 scripts/build-ssg.ts when scaling to >10k routes.

Advanced Plugin Architecture for SSR/SSG

Custom plugins must explicitly handle SSR contexts using configResolved and transform hooks. Framework maintainers should leverage Advanced Vite Plugin Configuration to implement conditional logic via this.environment?.mode === 'ssr'. Proper plugin scoping prevents client-only code from leaking into server bundles and ensures deterministic build outputs.

Environment-aware plugin example:

// plugins/ssr-hydrate.ts
import type { Plugin } from 'vite';

export function ssrHydratePlugin(): Plugin {
 return {
 name: 'vite:ssr-hydrate',
 apply: { ssr: true },
 enforce: 'pre',
 configResolved(config) {
 console.log(`SSR target: ${config.ssr?.target || 'node'}`);
 },
 transform(code, id) {
 if (id.includes('framework-core') && this.environment?.mode === 'ssr') {
 // Inject server-side bootstrapping
 return code.replace(/hydrateApp\(/, 'ssrBootstrap(');
 }
 return null;
 }
 };
}

Performance Impact: Scoping plugins to apply: { ssr: true } prevents unnecessary AST transformations in the client build, reducing Rollup compilation overhead by ~15%. Filtering resolveId calls for virtual modules eliminates redundant file system I/O during server graph construction.

Debugging Paths:

  • Use vite --debug plugin to trace hook execution order and verify environment context propagation.
  • Audit ssr.noExternal for unintended bundling of browser APIs (e.g., canvas, webgl) that trigger runtime crashes.
  • Verify plugin context isolation across client/server builds by logging this.environment?.name inside transform hooks.

Production Deployment & Performance Tuning

Finalizing SSR/SSG setups requires optimizing bundle splitting, caching strategies, and CDN edge compatibility. Rollup’s manualChunks configuration is critical for partitioning shared SSR dependencies, while HTTP/2 push alternatives and aggressive caching headers ensure predictable delivery. Memory profiling for long-running Node processes is mandatory to prevent heap exhaustion under concurrent load.

Production-optimized configuration:

// vite.config.prod.ts
import { defineConfig } from 'vite';

export default defineConfig({
 build: {
 minify: 'esbuild',
 ssrManifest: true,
 rollupOptions: {
 output: {
 manualChunks(id) {
 if (id.includes('node_modules')) {
 if (id.includes('react') || id.includes('vue')) return 'vendor-ssr';
 if (id.includes('lodash') || id.includes('date-fns')) return 'vendor-utils';
 }
 }
 }
 }
 },
 server: {
 hmr: false // Disable HMR for production SSR builds
 }
});

Performance Impact: Strategic manualChunks partitioning reduces server-side memory footprint by ~30% under concurrent request spikes by isolating frequently imported vendor modules. Using build.minify: 'esbuild' shaves 40–60% off production build times compared to Terser, while maintaining identical AST output. Setting server.hmr: false in production eliminates WebSocket overhead and reduces Node event loop tick latency.

Debugging Paths:

  • Profile Node heap snapshots during concurrent request spikes using clinic doctor -- node server.js to identify memory leaks in SSR caches.
  • Verify import.meta.env.SSR flag propagation in production builds by grepping the output directory: grep -r "import.meta.env.SSR" dist/.
  • Audit CDN cache invalidation for dynamic route updates by implementing Cache-Control: s-maxage=3600, stale-while-revalidate=60 headers on pre-rendered HTML responses.

In-Depth Guides