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
ssrManifestgeneration to identify hydration mismatches caused by missing chunk references. - Audit the dependency tree with
npm lsorpnpm whyto catch CJS/ESM boundary violations that trigger synchronousrequire()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_FOUNDin SSR context usingVITE_DEBUG=ssr vite dev. - Validate
window/documentpolyfills during server execution by runningnode --inspect server.tsand 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.jsonfor missing chunks that cause hydration failures. - Validate hydration state injection against the DOM structure using browser DevTools’ “Elements” panel to ensure
data-hydrationattributes align. - Profile build-time memory consumption with
node --max-old-space-size=4096 scripts/build-ssg.tswhen 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 pluginto trace hook execution order and verify environment context propagation. - Audit
ssr.noExternalfor 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?.nameinsidetransformhooks.
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.jsto identify memory leaks in SSR caches. - Verify
import.meta.env.SSRflag 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=60headers on pre-rendered HTML responses.