JavaScript Server-Side Rendering: Next.js, Nuxt.js, and Universal Applications
Master server-side rendering with JavaScript. Learn Next.js, Nuxt.js, hydration, static generation, and building universal applications.
Server-Side Rendering (SSR) with JavaScript enables building fast, SEO-friendly applications by rendering content on the server. Using frameworks like Next.js and Nuxt.js, developers can create universal applications that render on both server and client. This comprehensive guide covers SSR fundamentals, hydration strategies, static generation, and performance optimization.
SSR Fundamentals and Implementation
Universal JavaScript Engine
// Universal JavaScript Renderer
class UniversalRenderer {
constructor(options = {}) {
this.isServer = typeof window === 'undefined';
this.isClient = !this.isServer;
this.config = {
hydration: options.hydration !== false,
streaming: options.streaming || false,
caching: options.caching || true,
preloadData: options.preloadData || true,
};
this.componentRegistry = new Map();
this.routeRegistry = new Map();
this.middleware = [];
this.cache = new Map();
this.store = null;
if (this.isServer) {
this.setupServerRendering();
} else {
this.setupClientHydration();
}
}
// Register component for universal rendering
registerComponent(name, component, options = {}) {
const componentDef = {
name,
component,
options: {
ssr: options.ssr !== false,
preload: options.preload || null,
getServerSideProps: options.getServerSideProps || null,
getStaticProps: options.getStaticProps || null,
},
};
this.componentRegistry.set(name, componentDef);
}
// Register route
registerRoute(path, componentName, options = {}) {
const route = {
path: this.normalizePath(path),
componentName,
options: {
exact: options.exact !== false,
middleware: options.middleware || [],
preload: options.preload || [],
cache: options.cache || false,
cacheTTL: options.cacheTTL || 300000,
},
};
this.routeRegistry.set(path, route);
}
// Server-side rendering
async renderToString(url, context = {}) {
if (!this.isServer) {
throw new Error('renderToString can only be called on server');
}
const route = this.findRoute(url);
if (!route) {
throw new Error(`Route not found: ${url}`);
}
const componentDef = this.componentRegistry.get(route.componentName);
if (!componentDef) {
throw new Error(`Component not found: ${route.componentName}`);
}
try {
// Check cache
if (route.options.cache) {
const cached = this.getFromCache(url);
if (cached) {
return cached;
}
}
// Execute middleware
for (const middleware of route.options.middleware) {
await middleware(context);
}
// Preload data
const props = await this.preloadComponentData(componentDef, context);
// Render component
const html = await this.renderComponent(
componentDef.component,
props,
context
);
// Generate full HTML
const fullHTML = this.wrapHTML(html, props, context);
// Cache result
if (route.options.cache) {
this.setCache(url, fullHTML, route.options.cacheTTL);
}
return fullHTML;
} catch (error) {
console.error('SSR Error:', error);
throw error;
}
}
// Streaming SSR
async renderToStream(url, context = {}) {
if (!this.isServer) {
throw new Error('renderToStream can only be called on server');
}
const { Readable } = require('stream');
return new Readable({
async read() {
try {
const html = await this.renderToString(url, context);
this.push(html);
this.push(null); // End stream
} catch (error) {
this.destroy(error);
}
},
});
}
// Client-side hydration
async hydrate(containerId = 'app') {
if (!this.isClient) {
throw new Error('hydrate can only be called on client');
}
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container element not found: ${containerId}`);
}
try {
// Get hydration data
const hydrationData = this.getHydrationData();
// Initialize client state
if (hydrationData.store) {
this.store = this.createStore(hydrationData.store);
}
// Hydrate components
await this.hydrateComponents(container, hydrationData);
// Setup client-side routing
this.setupClientRouting();
console.log('Hydration completed');
} catch (error) {
console.error('Hydration error:', error);
throw error;
}
}
// Preload component data
async preloadComponentData(componentDef, context) {
let props = {};
// Get server-side props
if (componentDef.options.getServerSideProps) {
const serverProps =
await componentDef.options.getServerSideProps(context);
props = { ...props, ...serverProps };
}
// Get static props
if (componentDef.options.getStaticProps) {
const staticProps = await componentDef.options.getStaticProps(context);
props = { ...props, ...staticProps };
}
// Execute preload functions
if (componentDef.options.preload) {
const preloadPromises = componentDef.options.preload.map((fn) =>
fn(context)
);
const preloadResults = await Promise.all(preloadPromises);
props.preloadData = preloadResults;
}
return props;
}
// Render component to HTML
async renderComponent(component, props, context) {
// This is a simplified example - in practice, you'd use a library like React/Vue
if (typeof component === 'function') {
return component(props, context);
}
if (component.render) {
return component.render(props, context);
}
throw new Error('Component must be a function or have a render method');
}
// Wrap HTML with document structure
wrapHTML(html, props, context) {
const serializedProps = JSON.stringify(props).replace(/</g, '\\u003c');
const hydrationScript = this.config.hydration
? this.getHydrationScript(props)
: '';
return `
<!DOCTYPE html>
<html lang="${context.lang || 'en'}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${context.title || 'Universal App'}</title>
${context.meta || ''}
${context.styles || ''}
</head>
<body>
<div id="app">${html}</div>
<script id="__UNIVERSAL_DATA__" type="application/json">
${serializedProps}
</script>
${hydrationScript}
${context.scripts || ''}
</body>
</html>
`;
}
// Generate hydration script
getHydrationScript(props) {
return `
<script>
window.__UNIVERSAL_RENDERER__ = {
props: ${JSON.stringify(props)},
hydrated: false
};
// Auto-hydrate when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (window.universalRenderer) {
window.universalRenderer.hydrate();
}
});
} else {
if (window.universalRenderer) {
window.universalRenderer.hydrate();
}
}
</script>
`;
}
// Get hydration data on client
getHydrationData() {
const dataElement = document.getElementById('__UNIVERSAL_DATA__');
if (!dataElement) {
return {};
}
try {
return JSON.parse(dataElement.textContent);
} catch (error) {
console.error('Failed to parse hydration data:', error);
return {};
}
}
// Hydrate components on client
async hydrateComponents(container, hydrationData) {
const componentElements = container.querySelectorAll('[data-component]');
for (const element of componentElements) {
const componentName = element.getAttribute('data-component');
const componentDef = this.componentRegistry.get(componentName);
if (componentDef) {
try {
await this.hydrateComponent(element, componentDef, hydrationData);
} catch (error) {
console.error(`Failed to hydrate component ${componentName}:`, error);
}
}
}
}
async hydrateComponent(element, componentDef, hydrationData) {
// Component-specific hydration logic
if (componentDef.component.hydrate) {
await componentDef.component.hydrate(element, hydrationData);
} else {
// Default hydration - attach event listeners, etc.
this.attachEventListeners(element, componentDef);
}
}
attachEventListeners(element, componentDef) {
// Find and attach event listeners
const interactiveElements = element.querySelectorAll('[data-event]');
interactiveElements.forEach((el) => {
const eventType = el.getAttribute('data-event');
const handlerName = el.getAttribute('data-handler');
if (componentDef.component[handlerName]) {
el.addEventListener(eventType, componentDef.component[handlerName]);
}
});
}
// Setup server rendering
setupServerRendering() {
// Server-specific setup
this.renderingMode = 'server';
}
// Setup client hydration
setupClientHydration() {
// Client-specific setup
this.renderingMode = 'client';
// Make renderer globally available
if (typeof window !== 'undefined') {
window.universalRenderer = this;
}
}
// Client-side routing
setupClientRouting() {
if (!this.isClient) return;
// Simple history API routing
window.addEventListener('popstate', (event) => {
this.handleRouteChange(window.location.pathname);
});
// Intercept link clicks
document.addEventListener('click', (event) => {
const link = event.target.closest('a[href]');
if (link && link.origin === window.location.origin) {
event.preventDefault();
this.navigateTo(link.pathname);
}
});
}
async navigateTo(path) {
if (!this.isClient) return;
try {
// Update URL
window.history.pushState({}, '', path);
// Render new route
await this.handleRouteChange(path);
} catch (error) {
console.error('Navigation error:', error);
}
}
async handleRouteChange(path) {
const route = this.findRoute(path);
if (!route) {
console.error(`Route not found: ${path}`);
return;
}
const componentDef = this.componentRegistry.get(route.componentName);
if (!componentDef) {
console.error(`Component not found: ${route.componentName}`);
return;
}
// Client-side rendering
const context = { url: path, isClient: true };
const props = await this.preloadComponentData(componentDef, context);
const html = await this.renderComponent(
componentDef.component,
props,
context
);
// Update DOM
const container = document.getElementById('app');
if (container) {
container.innerHTML = html;
await this.hydrateComponents(container, props);
}
}
// Utility methods
findRoute(url) {
const pathname = new URL(url, 'http://localhost').pathname;
for (const [path, route] of this.routeRegistry) {
if (this.matchRoute(route.path, pathname)) {
return route;
}
}
return null;
}
matchRoute(routePath, pathname) {
if (routePath === pathname) return true;
// Simple pattern matching
const routeParts = routePath.split('/');
const pathParts = pathname.split('/');
if (routeParts.length !== pathParts.length) return false;
return routeParts.every((part, index) => {
return part.startsWith(':') || part === pathParts[index];
});
}
normalizePath(path) {
return path.startsWith('/') ? path : `/${path}`;
}
// Cache operations
getFromCache(key) {
const cached = this.cache.get(key);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
this.cache.delete(key);
return null;
}
setCache(key, data, ttl) {
this.cache.set(key, {
data,
expires: Date.now() + ttl,
});
}
}
Next.js Implementation Patterns
Advanced Next.js Architecture
// Next.js Enhanced Application Structure
class NextJSEnhancer {
constructor() {
this.plugins = new Map();
this.middleware = [];
this.dataFetchers = new Map();
this.cacheStrategies = new Map();
}
// Enhanced getServerSideProps wrapper
withServerSideProps(fetcher, options = {}) {
return async (context) => {
const startTime = Date.now();
try {
// Apply middleware
for (const middleware of this.middleware) {
await middleware(context);
}
// Check cache
if (options.cache) {
const cached = await this.getFromCache(
context.resolvedUrl,
options.cache
);
if (cached) {
return {
props: cached,
revalidate: options.revalidate,
};
}
}
// Execute data fetcher
const props = await fetcher(context);
// Cache result
if (options.cache) {
await this.setCache(context.resolvedUrl, props, options.cache);
}
// Add performance metrics
props._performance = {
serverSideTime: Date.now() - startTime,
timestamp: new Date().toISOString(),
};
return {
props,
revalidate: options.revalidate,
};
} catch (error) {
console.error('SSR Error:', error);
if (options.fallback) {
return {
props: options.fallback,
revalidate: 1, // Quick retry
};
}
throw error;
}
};
}
// Enhanced getStaticProps wrapper
withStaticProps(fetcher, options = {}) {
return async (context) => {
try {
const props = await fetcher(context);
return {
props,
revalidate: options.revalidate || 3600, // 1 hour default
notFound: options.notFound || false,
};
} catch (error) {
console.error('Static props error:', error);
if (options.fallback) {
return {
props: options.fallback,
revalidate: 60, // Quick retry for errors
};
}
return {
notFound: true,
};
}
};
}
// Enhanced getStaticPaths wrapper
withStaticPaths(fetcher, options = {}) {
return async () => {
try {
const paths = await fetcher();
return {
paths,
fallback: options.fallback || 'blocking',
};
} catch (error) {
console.error('Static paths error:', error);
return {
paths: [],
fallback: 'blocking',
};
}
};
}
// Progressive Web App enhancements
createPWAConfig() {
return {
pwa: {
dest: 'public',
register: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 365 days
},
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-static',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 365 days
},
},
},
{
urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-image-assets',
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /\.(?:js|css)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-js-css-assets',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /^\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 16,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
};
}
// Image optimization utilities
createImageOptimization() {
return {
// Custom loader for images
imageLoader: ({ src, width, quality }) => {
const params = new URLSearchParams();
params.set('url', src);
params.set('w', width.toString());
if (quality) {
params.set('q', quality.toString());
}
return `/api/image?${params}`;
},
// Image component with advanced features
OptimizedImage: ({ src, alt, priority = false, ...props }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState(false);
return (
<div className="relative">
{!isLoaded && !error && (
<div className="absolute inset-0 animate-pulse bg-gray-200" />
)}
<Image
src={src}
alt={alt}
priority={priority}
onLoad={() => setIsLoaded(true)}
onError={() => setError(true)}
{...props}
/>
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<span className="text-gray-500">Failed to load image</span>
</div>
)}
</div>
);
},
};
}
// SEO optimization utilities
createSEOUtils() {
return {
generateMetaTags: (data) => {
const meta = [];
if (data.title) {
meta.push(<title key="title">{data.title}</title>);
meta.push(
<meta key="og:title" property="og:title" content={data.title} />
);
meta.push(
<meta
key="twitter:title"
name="twitter:title"
content={data.title}
/>
);
}
if (data.description) {
meta.push(
<meta
key="description"
name="description"
content={data.description}
/>
);
meta.push(
<meta
key="og:description"
property="og:description"
content={data.description}
/>
);
meta.push(
<meta
key="twitter:description"
name="twitter:description"
content={data.description}
/>
);
}
if (data.image) {
meta.push(
<meta key="og:image" property="og:image" content={data.image} />
);
meta.push(
<meta
key="twitter:image"
name="twitter:image"
content={data.image}
/>
);
}
if (data.url) {
meta.push(<meta key="og:url" property="og:url" content={data.url} />);
}
// Additional SEO tags
meta.push(<meta key="og:type" property="og:type" content="website" />);
meta.push(
<meta
key="twitter:card"
name="twitter:card"
content="summary_large_image"
/>
);
return meta;
},
generateStructuredData: (type, data) => {
const structuredData = {
'@context': 'https://schema.org',
'@type': type,
...data,
};
return (
<script
key="structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData, null, 2),
}}
/>
);
},
};
}
// Performance monitoring
createPerformanceMonitor() {
return {
// Web Vitals tracking
trackWebVitals: (metric) => {
console.log('Web Vitals:', metric);
// Send to analytics
if (typeof gtag !== 'undefined') {
gtag('event', metric.name, {
event_category: 'Web Vitals',
value: Math.round(metric.value),
event_label: metric.id,
non_interaction: true,
});
}
},
// Performance observer
setupPerformanceObserver: () => {
if (typeof window !== 'undefined' && 'PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Performance entry:', entry);
}
});
observer.observe({
entryTypes: ['navigation', 'paint', 'largest-contentful-paint'],
});
}
},
// Bundle analysis
analyzeBundleSize: async () => {
if (process.env.ANALYZE) {
const { BundleAnalyzerPlugin } = await import(
'webpack-bundle-analyzer'
);
return new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html',
});
}
return null;
},
};
}
// Cache strategies
async getFromCache(key, strategy) {
switch (strategy.type) {
case 'redis':
return this.getFromRedis(key, strategy);
case 'memory':
return this.getFromMemory(key, strategy);
case 'cdn':
return this.getFromCDN(key, strategy);
default:
return null;
}
}
async setCache(key, data, strategy) {
switch (strategy.type) {
case 'redis':
return this.setToRedis(key, data, strategy);
case 'memory':
return this.setToMemory(key, data, strategy);
case 'cdn':
return this.setToCDN(key, data, strategy);
}
}
// Redis cache implementation
async getFromRedis(key, strategy) {
try {
const redis = require('redis');
const client = redis.createClient(strategy.options);
const data = await client.get(key);
await client.quit();
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Redis cache error:', error);
return null;
}
}
async setToRedis(key, data, strategy) {
try {
const redis = require('redis');
const client = redis.createClient(strategy.options);
await client.setex(key, strategy.ttl || 3600, JSON.stringify(data));
await client.quit();
} catch (error) {
console.error('Redis cache error:', error);
}
}
}
// Custom hooks for SSR
const useSSRSafeEffect = (effect, deps) => {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
useEffect(() => {
if (isClient) {
return effect();
}
}, [isClient, ...deps]);
};
const useHydrationSafe = (clientValue, serverValue = null) => {
const [value, setValue] = useState(serverValue);
useEffect(() => {
setValue(clientValue);
}, [clientValue]);
return value;
};
// Example usage with Next.js
const ExamplePage = ({ data, performance }) => {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
setHydrated(true);
}, []);
// Safe client-side operations
const clientSideValue = useHydrationSafe(
typeof window !== 'undefined' ? window.location.href : null,
''
);
return (
<div>
<h1>Universal Page</h1>
<p>Data: {JSON.stringify(data)}</p>
{hydrated && <p>Client URL: {clientSideValue}</p>}
<small>Server render time: {performance?.serverSideTime}ms</small>
</div>
);
};
// Enhanced getServerSideProps
export const getServerSideProps = withServerSideProps(
async (context) => {
// Fetch data
const data = await fetchData(context.params?.id);
return {
data,
timestamp: new Date().toISOString(),
};
},
{
cache: {
type: 'redis',
ttl: 300,
options: { host: 'localhost', port: 6379 },
},
revalidate: 60,
fallback: { data: null, error: 'Failed to load data' },
}
);
Static Site Generation and ISR
Incremental Static Regeneration System
// ISR (Incremental Static Regeneration) Manager
class ISRManager {
constructor(options = {}) {
this.config = {
revalidateInterval: options.revalidateInterval || 60,
maxStaleAge: options.maxStaleAge || 86400,
cacheDirectory: options.cacheDirectory || '.next/cache',
buildQueue: options.buildQueue || 'memory',
};
this.buildQueue = new Map();
this.buildStatus = new Map();
this.cacheHeaders = new Map();
}
// Generate static page with ISR
async generateStaticPage(path, context, getStaticProps) {
const cacheKey = this.getCacheKey(path, context);
const cachedPage = await this.getCachedPage(cacheKey);
// Check if revalidation is needed
const shouldRevalidate = this.shouldRevalidate(cachedPage);
if (cachedPage && !shouldRevalidate) {
// Serve from cache
return {
html: cachedPage.html,
props: cachedPage.props,
headers: this.getCacheHeaders(cachedPage),
};
}
if (cachedPage && shouldRevalidate) {
// Serve stale while revalidating
this.queueRevalidation(path, context, getStaticProps);
return {
html: cachedPage.html,
props: cachedPage.props,
headers: this.getStaleHeaders(cachedPage),
};
}
// Generate new page
return this.buildPage(path, context, getStaticProps);
}
// Build page from scratch
async buildPage(path, context, getStaticProps) {
const buildKey = `${path}:${JSON.stringify(context)}`;
// Check if already building
if (this.buildStatus.has(buildKey)) {
return this.waitForBuild(buildKey);
}
// Mark as building
this.buildStatus.set(buildKey, 'building');
try {
// Execute getStaticProps
const staticPropsResult = await getStaticProps(context);
if (staticPropsResult.notFound) {
return { notFound: true };
}
if (staticPropsResult.redirect) {
return { redirect: staticPropsResult.redirect };
}
const { props, revalidate } = staticPropsResult;
// Render page to HTML
const html = await this.renderPageToHTML(path, props, context);
// Cache the result
const cacheEntry = {
html,
props,
revalidate: revalidate || this.config.revalidateInterval,
generatedAt: Date.now(),
path,
context,
};
await this.cachePage(this.getCacheKey(path, context), cacheEntry);
// Mark as completed
this.buildStatus.set(buildKey, 'completed');
return {
html,
props,
headers: this.getFreshHeaders(cacheEntry),
};
} catch (error) {
this.buildStatus.set(buildKey, 'failed');
console.error('Page build failed:', error);
throw error;
} finally {
// Clean up build status after delay
setTimeout(() => {
this.buildStatus.delete(buildKey);
}, 60000);
}
}
// Queue revalidation in background
queueRevalidation(path, context, getStaticProps) {
const queueKey = `${path}:${JSON.stringify(context)}`;
if (this.buildQueue.has(queueKey)) {
return; // Already queued
}
this.buildQueue.set(queueKey, {
path,
context,
getStaticProps,
queuedAt: Date.now(),
});
// Process queue
setImmediate(() => {
this.processRevalidationQueue();
});
}
// Process revalidation queue
async processRevalidationQueue() {
const batch = Array.from(this.buildQueue.entries()).slice(0, 3); // Process 3 at a time
await Promise.all(
batch.map(async ([queueKey, item]) => {
try {
this.buildQueue.delete(queueKey);
await this.buildPage(item.path, item.context, item.getStaticProps);
console.log(`Revalidated: ${item.path}`);
} catch (error) {
console.error(`Revalidation failed for ${item.path}:`, error);
}
})
);
// Continue processing if queue has items
if (this.buildQueue.size > 0) {
setTimeout(() => {
this.processRevalidationQueue();
}, 1000);
}
}
// Check if page should be revalidated
shouldRevalidate(cachedPage) {
if (!cachedPage) return true;
const age = (Date.now() - cachedPage.generatedAt) / 1000;
return age > cachedPage.revalidate;
}
// Render page to HTML
async renderPageToHTML(path, props, context) {
// This is simplified - in practice, you'd use Next.js rendering
const PageComponent = await this.getPageComponent(path);
if (!PageComponent) {
throw new Error(`Page component not found for path: ${path}`);
}
// Render with React Server Components or similar
return this.renderToString(PageComponent, props, context);
}
async renderToString(Component, props, context) {
// Simplified rendering - use actual framework renderer
try {
if (typeof Component === 'function') {
const element = Component(props);
return this.stringifyElement(element);
}
throw new Error('Invalid component');
} catch (error) {
console.error('Render error:', error);
return '<div>Render Error</div>';
}
}
stringifyElement(element) {
// Very simplified HTML generation
if (typeof element === 'string') return element;
if (typeof element === 'number') return element.toString();
if (!element) return '';
if (element.type && element.props) {
const { type, props } = element;
const children = props.children || '';
return `<${type}>${this.stringifyElement(children)}</${type}>`;
}
return JSON.stringify(element);
}
// Cache management
async getCachedPage(cacheKey) {
try {
const fs = require('fs').promises;
const path = require('path');
const cachePath = path.join(
this.config.cacheDirectory,
`${cacheKey}.json`
);
const data = await fs.readFile(cachePath, 'utf8');
return JSON.parse(data);
} catch (error) {
return null; // Cache miss
}
}
async cachePage(cacheKey, cacheEntry) {
try {
const fs = require('fs').promises;
const path = require('path');
// Ensure cache directory exists
await fs.mkdir(this.config.cacheDirectory, { recursive: true });
const cachePath = path.join(
this.config.cacheDirectory,
`${cacheKey}.json`
);
await fs.writeFile(cachePath, JSON.stringify(cacheEntry, null, 2));
} catch (error) {
console.error('Cache write error:', error);
}
}
getCacheKey(path, context) {
const key = `${path}:${JSON.stringify(context.params || {})}`;
return key.replace(/[^a-zA-Z0-9]/g, '_');
}
// HTTP cache headers
getCacheHeaders(cachedPage) {
const age = Math.floor((Date.now() - cachedPage.generatedAt) / 1000);
const maxAge = cachedPage.revalidate;
return {
'Cache-Control': `public, max-age=${maxAge}, s-maxage=${maxAge}`,
Age: age.toString(),
'X-Cache': 'HIT',
};
}
getStaleHeaders(cachedPage) {
const age = Math.floor((Date.now() - cachedPage.generatedAt) / 1000);
const maxAge = cachedPage.revalidate;
return {
'Cache-Control': `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=86400`,
Age: age.toString(),
'X-Cache': 'STALE',
};
}
getFreshHeaders(cachedPage) {
return {
'Cache-Control': `public, max-age=${cachedPage.revalidate}, s-maxage=${cachedPage.revalidate}`,
Age: '0',
'X-Cache': 'MISS',
};
}
// Utility methods
async getPageComponent(path) {
// Dynamic import of page component
try {
const component = await import(`../pages${path}`);
return component.default || component;
} catch (error) {
console.error(`Failed to load component for ${path}:`, error);
return null;
}
}
async waitForBuild(buildKey) {
return new Promise((resolve, reject) => {
const checkStatus = () => {
const status = this.buildStatus.get(buildKey);
if (status === 'completed') {
resolve({ success: true });
} else if (status === 'failed') {
reject(new Error('Build failed'));
} else {
setTimeout(checkStatus, 100);
}
};
checkStatus();
});
}
// Monitoring and analytics
getStatistics() {
return {
queueSize: this.buildQueue.size,
buildingPages: Array.from(this.buildStatus.entries()).filter(
([_, status]) => status === 'building'
).length,
cacheDirectory: this.config.cacheDirectory,
revalidateInterval: this.config.revalidateInterval,
};
}
}
// Edge function for ISR
const edgeISRHandler = async (request, context) => {
const url = new URL(request.url);
const path = url.pathname;
const isrManager = new ISRManager({
revalidateInterval: 60,
cacheDirectory: '/tmp/cache',
});
try {
const result = await isrManager.generateStaticPage(
path,
{ params: context.params },
getStaticProps
);
if (result.notFound) {
return new Response('Not Found', { status: 404 });
}
if (result.redirect) {
return Response.redirect(
result.redirect.destination,
result.redirect.permanent ? 301 : 302
);
}
return new Response(result.html, {
headers: {
'Content-Type': 'text/html',
...result.headers,
},
});
} catch (error) {
console.error('ISR handler error:', error);
return new Response('Internal Server Error', { status: 500 });
}
};
This comprehensive guide covers JavaScript server-side rendering from universal rendering engines to advanced Next.js patterns and incremental static regeneration. The examples provide production-ready implementations for building fast, SEO-friendly applications with optimal performance and user experience.