Modern Rendering Strategies Implementation
1. Next.js 14 App Router SSR SSG NEW
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| App Router | app/ directory structure |
File-system based routing with layouts and nested routes | Modern Next.js architecture |
| Server Components | async function Page() {} |
Components that render on server by default | Zero JS to client, SEO |
| generateStaticParams | export async function generateStaticParams() |
Static generation of dynamic routes at build time | SSG for dynamic routes |
| Dynamic Rendering | export const dynamic = 'force-dynamic' |
SSR on every request, no caching | Personalized content |
| Revalidation | export const revalidate = 60 |
Incremental Static Regeneration (ISR) interval | Periodic content updates |
| Loading UI | loading.tsx |
Instant loading state while streaming | Progressive rendering |
| Error Boundaries | error.tsx |
Route-level error handling | Graceful error recovery |
Example: Next.js 14 App Router with SSR, SSG, and ISR
// app/layout.tsx - Root layout with metadata
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'Next.js 14 App Router Example',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<nav>Navigation</nav>
{children}
</body>
</html>
);
}
// app/page.tsx - Static page (SSG by default)
export default async function HomePage() {
const data = await fetch('https://api.example.com/posts', {
cache: 'force-cache', // SSG: cached at build time
}).then(res => res.json());
return (
<div>
<h1>Home Page (Static)</h1>
{data.map(post => <div key={post.id}>{post.title}</div>)}
</div>
);
}
// app/posts/[id]/page.tsx - Dynamic route with SSG
interface PageProps {
params: { id: string };
}
// Generate static paths at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map((post: any) => ({
id: post.id.toString(),
}));
}
// Revalidate every 60 seconds (ISR)
export const revalidate = 60;
export default async function PostPage({ params }: PageProps) {
const post = await fetch(`https://api.example.com/posts/${params.id}`, {
next: { revalidate: 60 }, // Per-fetch revalidation
}).then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// app/dashboard/page.tsx - Dynamic SSR (no cache)
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const user = await getCurrentUser();
const stats = await fetch(`https://api.example.com/stats/${user.id}`, {
cache: 'no-store', // SSR: fetch on every request
}).then(res => res.json());
return (
<div>
<h1>Dashboard (Dynamic SSR)</h1>
<p>Views: {stats.views}</p>
</div>
);
}
// app/products/loading.tsx - Loading state
export default function Loading() {
return <div>Loading products...</div>;
}
// app/products/error.tsx - Error boundary
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// Client Component with 'use client'
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
2. Vite React SPA Client Rendering
| Feature | Configuration | Description | Benefit |
|---|---|---|---|
| HMR | Hot Module Replacement |
Instant updates without full page reload | Fast development |
| ESBuild | transform: { jsx: 'react' } |
Lightning-fast JavaScript/TypeScript compilation | 10-100x faster than webpack |
| Code Splitting | import() |
Dynamic imports for lazy loading | Smaller initial bundle |
| Tree Shaking | build.rollupOptions |
Dead code elimination in production | Optimized bundle size |
| CSS Modules | .module.css |
Scoped CSS with automatic class names | No style conflicts |
| Environment Variables | import.meta.env.VITE_* |
Access env vars prefixed with VITE_ | Configuration management |
Example: Vite React SPA with optimized configuration
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react({
// Fast Refresh for HMR
fastRefresh: true,
}),
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
build: {
// Target modern browsers
target: 'esnext',
// Manual chunk splitting
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@mui/material'],
},
},
},
// Source maps for production debugging
sourcemap: true,
// Chunk size warning limit
chunkSizeWarningLimit: 1000,
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@utils': '/src/utils',
},
},
});
// src/main.tsx - Entry point
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// src/App.tsx - Lazy loading routes
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;
// .env - Environment variables
VITE_API_URL=https://api.example.com
VITE_API_KEY=abc123
// Using environment variables
const apiUrl = import.meta.env.VITE_API_URL;
const apiKey = import.meta.env.VITE_API_KEY;
// CSS Modules - Button.module.css
.button {
padding: 10px 20px;
background: blue;
}
// Button.tsx
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>Click Me</button>;
}
3. Nuxt 3 Universal Rendering
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| Universal Rendering | SSR + Hydration |
Server renders HTML, client hydrates with interactivity | Best of both worlds |
| Auto Imports | No import statements needed |
Components, composables auto-imported | Less boilerplate |
| useFetch | const { data } = await useFetch('/api') |
Universal data fetching with SSR support | Isomorphic data fetching |
| useAsyncData | useAsyncData('key', () => $fetch()) |
Manual async data fetching with caching | Complex data needs |
| Server Routes | server/api/*.ts |
API routes built into Nuxt | Full-stack framework |
| Middleware | middleware/*.ts |
Route guards for auth, validation | Navigation control |
Example: Nuxt 3 universal rendering with data fetching
// nuxt.config.ts
export default defineNuxtConfig({
// Rendering mode
ssr: true,
// Hybrid rendering per route
routeRules: {
'/': { prerender: true }, // SSG
'/admin/**': { ssr: false }, // SPA
'/api/**': { cors: true },
'/blog/**': { swr: 3600 }, // ISR with 1h cache
},
// App configuration
app: {
head: {
title: 'Nuxt 3 App',
meta: [
{ name: 'description', content: 'Universal rendering example' }
],
},
},
// Modules
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
// Auto imports
imports: {
dirs: ['composables', 'utils'],
},
});
// pages/index.vue - SSR page with data fetching
<template>
<div>
<h1>Posts</h1>
<div v-for="post in posts" :key="post.id">
<h2>{{ post.title }}</h2>
<p>{{ post.content }}</p>
</div>
</div>
</template>
<script setup>
// Auto-imported useFetch
const { data: posts } = await useFetch('/api/posts');
// SEO metadata
useHead({
title: 'Blog Posts',
meta: [
{ name: 'description', content: 'Latest blog posts' }
],
});
</script>
// pages/post/[id].vue - Dynamic route with SSR
<template>
<article>
<h1>{{ post?.title }}</h1>
<p>{{ post?.content }}</p>
</article>
</template>
<script setup>
const route = useRoute();
const { data: post } = await useAsyncData(
`post-${route.params.id}`,
() => $fetch(`/api/posts/${route.params.id}`)
);
</script>
// server/api/posts.ts - API route
export default defineEventHandler(async (event) => {
const posts = await fetchPostsFromDB();
return posts;
});
// server/api/posts/[id].ts - Dynamic API route
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const post = await fetchPostById(id);
if (!post) {
throw createError({
statusCode: 404,
statusMessage: 'Post not found',
});
}
return post;
});
// composables/useAuth.ts - Auto-imported composable
export const useAuth = () => {
const user = useState('user', () => null);
const login = async (credentials) => {
const data = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials,
});
user.value = data.user;
};
const logout = () => {
user.value = null;
};
return { user, login, logout };
};
// middleware/auth.ts - Route middleware
export default defineNuxtRouteMiddleware((to, from) => {
const { user } = useAuth();
if (!user.value && to.path !== '/login') {
return navigateTo('/login');
}
});
// pages/dashboard.vue - Protected route
<template>
<div>Dashboard for {{ user?.name }}</div>
</template>
<script setup>
definePageMeta({
middleware: 'auth',
});
const { user } = useAuth();
</script>
4. React 18 Concurrent Features NEW
| Feature | API | Description | Use Case |
|---|---|---|---|
| Concurrent Rendering | createRoot() |
Interruptible rendering for better UX | Responsive UI |
| useTransition | const [isPending, startTransition] = useTransition() |
Marks updates as non-urgent, allows interruption | Smooth UI transitions |
| useDeferredValue | const deferredValue = useDeferredValue(value) |
Defers value updates to prioritize urgent renders | Debounced rendering |
| Suspense | <Suspense fallback={...}> |
Declarative loading states for async components | Code splitting, data fetching |
| startTransition | startTransition(() => setState()) |
Marks state updates as low-priority | Heavy computations |
| useId | const id = useId() |
Generates unique IDs for SSR compatibility | Accessible forms |
Example: React 18 concurrent features in action
// main.tsx - Concurrent mode with createRoot
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(<App />);
// useTransition for non-blocking updates
import { useState, useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => {
setQuery(value); // Urgent: update input immediately
// Non-urgent: expensive search operation
startTransition(() => {
const filtered = expensiveSearch(value);
setResults(filtered);
});
};
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
{isPending && <div>Searching...</div>}
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
// useDeferredValue for responsive UI
function ProductList({ searchQuery }: { searchQuery: string }) {
// Deferred value updates slower, keeps input responsive
const deferredQuery = useDeferredValue(searchQuery);
const [products, setProducts] = useState([]);
useEffect(() => {
// Heavy filtering operation uses deferred value
const filtered = expensiveFilter(deferredQuery);
setProducts(filtered);
}, [deferredQuery]);
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
// Suspense for code splitting and data fetching
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading component...</div>}>
<HeavyComponent />
</Suspense>
);
}
// Suspense with data fetching (using libraries like Relay, React Query)
function UserProfile({ userId }: { userId: string }) {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileDetails userId={userId} />
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
</Suspense>
);
}
// useId for SSR-safe unique IDs
function FormField({ label }: { label: string }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
// Combining concurrent features
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [isPending, startTransition] = useTransition();
const handleTabChange = (tab: string) => {
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
<nav>
<button onClick={() => handleTabChange('overview')}>Overview</button>
<button onClick={() => handleTabChange('analytics')}>Analytics</button>
<button onClick={() => handleTabChange('reports')}>Reports</button>
</nav>
{isPending && <div>Loading...</div>}
<Suspense fallback={<div>Loading content...</div>}>
<TabContent tab={activeTab} />
</Suspense>
</div>
);
}
// Automatic batching (React 18)
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// Both updates batched automatically (even in async)
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// Only one re-render
}, 1000);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
5. Virtual Scrolling react-window
| Component | Syntax | Description | Use Case |
|---|---|---|---|
| FixedSizeList | <FixedSizeList height={} itemCount={} itemSize={}> |
List with fixed-height items | Uniform item heights |
| VariableSizeList | <VariableSizeList itemSize={(index) => {}}> |
List with dynamic item heights | Variable content sizes |
| FixedSizeGrid | <FixedSizeGrid columnCount={} rowCount={}> |
Grid with fixed cell sizes | Spreadsheets, tables |
| Windowing | Only renders visible items | Renders only what's in viewport + buffer | Performance with huge lists |
| react-virtuoso | <Virtuoso data={} itemContent={}> |
Advanced virtualization with features | Complex scrolling needs |
Example: Virtual scrolling for large datasets
import { FixedSizeList, VariableSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
// Fixed-size list
function VirtualList({ items }: { items: any[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Variable-size list
function VariableList({ items }: { items: any[] }) {
const itemSizes = useRef<number[]>([]);
const getItemSize = (index: number) => {
// Calculate or cache item heights
return itemSizes.current[index] || 50;
};
const Row = ({ index, style }: any) => {
const rowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (rowRef.current) {
itemSizes.current[index] = rowRef.current.offsetHeight;
}
}, [index]);
return (
<div ref={rowRef} style={style}>
{items[index].content}
</div>
);
};
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
// With AutoSizer for responsive dimensions
function ResponsiveList({ items }: { items: any[] }) {
return (
<div style={{ height: '100vh' }}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
width={width}
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
)}
</AutoSizer>
</div>
);
}
// Infinite scroll with react-window
function InfiniteList({ loadMore }: { loadMore: () => Promise<void> }) {
const [items, setItems] = useState<any[]>([]);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const loadMoreItems = async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
const newItems = await loadMore();
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setIsLoading(false);
};
const isItemLoaded = (index: number) => !hasMore || index < items.length;
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasMore ? items.length + 1 : items.length}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{isItemLoaded(index) ? items[index].name : 'Loading...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
// react-virtuoso (alternative with better features)
import { Virtuoso } from 'react-virtuoso';
function VirtuosoList({ items }: { items: any[] }) {
return (
<Virtuoso
style={{ height: '600px' }}
data={items}
itemContent={(index, item) => (
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
)}
endReached={() => console.log('Reached end')}
/>
);
}
6. Streaming SSR Suspense Components
| Feature | Implementation | Description | Benefit |
|---|---|---|---|
| Streaming SSR | renderToPipeableStream() |
Sends HTML chunks as they're ready | Faster Time to First Byte |
| Selective Hydration | React 18 automatic |
Hydrates components in priority order | Interactive sooner |
| Suspense Boundaries | <Suspense fallback={...}> |
Stream different parts independently | Progressive enhancement |
| Server Components | Next.js 13+ RSC |
Zero-JS components on server | Reduced bundle size |
| Progressive Hydration | Load JS incrementally |
Prioritize visible/interactive parts | Better perceived performance |
Example: Streaming SSR with Suspense boundaries
// server.js - Node.js streaming SSR
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html');
const { pipe, abort } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
// Send initial shell immediately
res.statusCode = 200;
pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send('<h1>Server Error</h1>');
},
onError(error) {
console.error('Streaming error:', error);
},
});
setTimeout(abort, 10000); // Abort after 10s
});
// App.tsx - Multiple Suspense boundaries
function App() {
return (
<html>
<head>
<title>Streaming SSR</title>
</head>
<body>
<nav>Navigation (renders immediately)</nav>
{/* High priority content */}
<Suspense fallback={<HeroSkeleton />}>
<Hero />
</Suspense>
{/* Stream independently */}
<Suspense fallback={<ProductsSkeleton />}>
<Products />
</Suspense>
{/* Lower priority, loads last */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
<footer>Footer (renders immediately)</footer>
</body>
</html>
);
}
// Async component with data fetching
function Products() {
const products = use(fetchProducts()); // React 19 use() hook
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
// Next.js 13+ App Router with streaming
// app/page.tsx
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* Fast content renders first */}
<Suspense fallback={<Skeleton />}>
<FastComponent />
</Suspense>
{/* Slow content streams later */}
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</div>
);
}
// Server Component (no JS sent to client)
async function FastComponent() {
const data = await fetch('https://api.example.com/fast').then(r => r.json());
return <div>{data.title}</div>;
}
// Slow async component
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000));
const data = await fetch('https://api.example.com/slow').then(r => r.json());
return <div>{data.content}</div>;
}
// Client Component for interactivity
'use client';
import { useState } from 'react';
function InteractiveWidget() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Streaming timeline visualization:
// Time 0ms: HTML shell + nav + footer sent
// Time 100ms: Hero content streams in
// Time 500ms: Products stream in
// Time 2000ms: Comments stream in
// User sees content progressively, not all at once
Rendering Strategy Comparison
| Strategy | Initial Load | SEO | Interactivity | Best For |
|---|---|---|---|---|
| SSR (Next.js) | Fast (pre-rendered) | Excellent | Hydration delay | Content-heavy, SEO-critical |
| SSG (Static) | Fastest (CDN) | Excellent | Hydration delay | Blogs, docs, marketing |
| SPA (Vite) | Slow (blank page) | Poor | Instant | Web apps, dashboards |
| Streaming SSR | Progressive | Excellent | Progressive | Large pages, mixed content |
| ISR (Next.js) | Fast (cached) | Excellent | Hydration delay | E-commerce, dynamic content |