Server-Side Rendering vs Static Site Generation: Pilih Mana untuk Project 2025?
Panduan lengkap memilih antara SSR dan SSG untuk project web 2025. Perbandingan Next.js, Nuxt.js, Astro, dan framework modern lainnya dengan use cases praktis.

⚡ Server-Side Rendering vs Static Site Generation: Pilih Mana untuk Project 2025?
Dalam dunia web development modern, memilih rendering strategy yang tepat adalah keputusan krusial yang akan mempengaruhi performance, SEO, user experience, dan development workflow. Di tahun 2025, pilihan antara Server-Side Rendering (SSR) dan Static Site Generation (SSG) semakin kompleks dengan munculnya hybrid approaches dan edge computing.
“The best rendering strategy is the one that fits your specific use case, not the one that’s trending.” - Vercel Team
Mari kita explore secara mendalam kapan menggunakan SSR, SSG, atau kombinasi keduanya!
🎯 Memahami Rendering Strategies
1. Client-Side Rendering (CSR)
// Traditional SPA - Client-Side Rendering
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching happens on client
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}
// Pros: Interactive, fast navigation after initial load
// Cons: Poor SEO, slow initial load, blank page during loading
2. Server-Side Rendering (SSR)
// Next.js SSR Example
export async function getServerSideProps(context) {
// This runs on every request
const { params, query, req, res } = context;
// Fetch data on server
const response = await fetch(`${process.env.API_URL}/data/${params.id}`);
const data = await response.json();
// Handle authentication
const user = await getUser(req);
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
return {
props: {
data,
user,
timestamp: Date.now(), // Always fresh
},
};
}
function ProductPage({ data, user, timestamp }) {
return (
<div>
<h1>{data.title}</h1>
<p>Welcome, {user.name}</p>
<p>Last updated: {new Date(timestamp).toLocaleString()}</p>
<ProductDetails product={data} />
</div>
);
}
// Pros: Always fresh data, good SEO, personalized content
// Cons: Slower response time, server load, requires server
3. Static Site Generation (SSG)
// Next.js SSG Example
export async function getStaticProps() {
// This runs at build time
const posts = await fetch(`${process.env.API_URL}/posts`).then(res => res.json());
return {
props: {
posts,
},
// Regenerate page every 60 seconds if there's a request
revalidate: 60,
};
}
export async function getStaticPaths() {
// Generate paths at build time
const posts = await fetch(`${process.env.API_URL}/posts`).then(res => res.json());
const paths = posts.map(post => ({
params: { slug: post.slug },
}));
return {
paths,
fallback: 'blocking', // Generate missing pages on-demand
};
}
function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Pros: Lightning fast, excellent SEO, CDN cacheable
// Cons: Build time increases, data can be stale
🚀 Framework Comparison 2025
1. Next.js - The Versatile Champion
Rating: ⭐⭐⭐⭐⭐ (5/5)
// Next.js 14 - App Router with Server Components
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { ProductDetails } from './ProductDetails';
import { Reviews } from './Reviews';
import { RecommendedProducts } from './RecommendedProducts';
// Server Component - runs on server
async function ProductPage({ params }: { params: { id: string } }) {
// This fetch happens on the server
const product = await fetch(`${process.env.API_URL}/products/${params.id}`, {
cache: 'force-cache', // Static generation
}).then(res => res.json());
return (
<div>
<ProductDetails product={product} />
{/* Streaming with Suspense */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<RecommendedProducts category={product.category} />
</Suspense>
</div>
);
}
// Client Component for interactivity
'use client';
function AddToCartButton({ productId }: { productId: string }) {
const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
await addToCart(productId);
setIsAdding(false);
};
return (
<button onClick={handleAddToCart} disabled={isAdding}>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
Fitur Unggulan Next.js 14:
- App Router - File-based routing dengan layouts
- Server Components - Zero JavaScript untuk static content
- Streaming - Progressive page loading
- Edge Runtime - Deploy ke edge locations
- Turbopack - Ultra-fast bundler (beta)
2. Nuxt.js - Vue’s Powerhouse
Rating: ⭐⭐⭐⭐⭐ (5/5)
<!-- pages/products/[id].vue -->
<template>
<div>
<ProductHero :product="product" />
<!-- Lazy load components -->
<LazyProductReviews :product-id="product.id" />
<LazyRecommendedProducts :category="product.category" />
</div>
</template>
<script setup>
// Nuxt 3 - Auto-imports and composables
const route = useRoute();
const { data: product } = await $fetch(`/api/products/${route.params.id}`);
// SEO optimization
useSeoMeta({
title: product.name,
description: product.description,
ogImage: product.image,
});
// Preload critical data
await Promise.all([
$fetch(`/api/products/${route.params.id}/reviews`),
$fetch(`/api/products/recommended/${product.category}`)
]);
</script>
Fitur Unggulan Nuxt 3:
- Auto-imports - No more import statements
- Server Engine - Nitro untuk universal deployment
- Hybrid Rendering - Mix SSR, SSG, SPA per route
- DevTools - Built-in development tools
3. Astro - The Content-First Framework
Rating: ⭐⭐⭐⭐⭐ (5/5)
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await Astro.glob('../content/blog/*.md');
return posts.map(post => ({
params: { slug: post.frontmatter.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<html>
<head>
<title>{post.frontmatter.title}</title>
<meta name="description" content={post.frontmatter.description} />
</head>
<body>
<article>
<h1>{post.frontmatter.title}</h1>
<time>{post.frontmatter.publishedDate}</time>
<!-- Zero JS by default -->
<Content />
<!-- Interactive component only when needed -->
<CommentSection client:load />
<ShareButtons client:visible />
</article>
</body>
</html>
Fitur Unggulan Astro:
- Zero JavaScript - Ship only what’s needed
- Island Architecture - Selective hydration
- Framework Agnostic - Use React, Vue, Svelte together
- Content Collections - Type-safe content management
4. SvelteKit - The Lightweight Contender
Rating: ⭐⭐⭐⭐ (4/5)
// src/routes/products/[id]/+page.server.js
export async function load({ params, fetch }) {
const product = await fetch(`/api/products/${params.id}`).then(r => r.json());
return {
product,
// Stream additional data
reviews: fetch(`/api/products/${params.id}/reviews`).then(r => r.json())
};
}
<!-- src/routes/products/[id]/+page.svelte -->
<script>
export let data;
// Reactive statements
$: product = data.product;
$: reviews = data.reviews;
</script>
<main>
<ProductDetails {product} />
{#await reviews}
<ReviewsSkeleton />
{:then reviewsData}
<Reviews reviews={reviewsData} />
{:catch error}
<ErrorMessage {error} />
{/await}
</main>
📊 Performance Comparison
Benchmark Results (2025)
Framework | Build Time | Bundle Size | First Load | Hydration | SEO Score |
---|---|---|---|---|---|
Next.js 14 | 45s | 85KB | 1.2s | 200ms | 98/100 |
Nuxt 3 | 38s | 92KB | 1.1s | 180ms | 97/100 |
Astro | 25s | 12KB | 0.8s | 0ms | 100/100 |
SvelteKit | 32s | 45KB | 0.9s | 150ms | 99/100 |
Gatsby | 120s | 78KB | 1.0s | 250ms | 96/100 |
Real-World Performance
// Performance monitoring setup
const performanceMetrics = {
nextjs: {
TTFB: 180, // Time to First Byte
FCP: 1200, // First Contentful Paint
LCP: 1800, // Largest Contentful Paint
CLS: 0.05, // Cumulative Layout Shift
FID: 45 // First Input Delay
},
astro: {
TTFB: 120,
FCP: 800,
LCP: 1200,
CLS: 0.02,
FID: 25
},
nuxt: {
TTFB: 160,
FCP: 1100,
LCP: 1700,
CLS: 0.04,
FID: 40
}
};
🎯 Use Cases dan Decision Matrix
1. E-commerce Platform
// Hybrid approach for e-commerce
const ecommerceStrategy = {
// Static pages - SSG
homepage: 'SSG', // Marketing content
categoryPages: 'SSG + ISR', // Product listings
productPages: 'SSG + ISR', // Product details
// Dynamic pages - SSR
userDashboard: 'SSR', // Personalized content
checkout: 'SSR', // Real-time inventory
orderHistory: 'SSR', // User-specific data
// Client-side - SPA
cart: 'CSR', // Interactive features
wishlist: 'CSR', // User interactions
search: 'CSR' // Real-time search
};
// Next.js implementation
// pages/products/[slug].js
export async function getStaticProps({ params }) {
const product = await getProduct(params.slug);
return {
props: { product },
revalidate: 3600, // Revalidate every hour
};
}
// pages/dashboard/index.js
export async function getServerSideProps({ req }) {
const user = await getUser(req);
const orders = await getUserOrders(user.id);
return {
props: { user, orders },
};
}
2. Content Website/Blog
// Astro for content-heavy sites
// src/pages/blog/[...slug].astro
---
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content, headings } = await post.render();
---
<BlogLayout title={post.data.title}>
<!-- Table of contents -->
<TableOfContents {headings} />
<!-- Main content - zero JS -->
<Content />
<!-- Interactive features only when needed -->
<CommentSection client:visible />
<ShareButtons client:idle />
<NewsletterSignup client:media="(min-width: 768px)" />
</BlogLayout>
3. SaaS Application
// Nuxt 3 for SaaS with hybrid rendering
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
routeRules: {
// Marketing pages - prerendered
'/': { prerender: true },
'/pricing': { prerender: true },
'/features': { prerender: true },
// App pages - SSR with caching
'/dashboard/**': { ssr: true, headers: { 'cache-control': 's-maxage=60' } },
'/settings/**': { ssr: true },
// API routes - edge functions
'/api/**': { cors: true, headers: { 'access-control-allow-origin': '*' } }
}
}
});
// pages/dashboard/index.vue
<script setup>
// Server-side data fetching
const { data: user } = await $fetch('/api/user');
const { data: analytics } = await $fetch('/api/analytics');
// Client-side reactivity
const selectedPeriod = ref('7d');
const chartData = computed(() =>
analytics.value.filter(d => d.period === selectedPeriod.value)
);
</script>
🛠️ Implementation Strategies
1. Incremental Static Regeneration (ISR)
// Next.js ISR implementation
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return {
props: { posts },
// Regenerate at most once every 60 seconds
revalidate: 60,
};
}
// On-demand revalidation
// pages/api/revalidate.js
export default async function handler(req, res) {
if (req.query.secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
// Revalidate specific pages
await res.revalidate('/');
await res.revalidate('/blog');
await res.revalidate(`/blog/${req.query.slug}`);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
2. Edge-Side Rendering (ESR)
// Vercel Edge Functions
export const config = {
runtime: 'edge',
};
export default async function handler(request) {
const { searchParams } = new URL(request.url);
const country = request.geo?.country || 'US';
const city = request.geo?.city || 'Unknown';
// Personalize content based on location
const content = await fetch(`https://api.example.com/content?country=${country}`)
.then(res => res.json());
return new Response(
JSON.stringify({
content,
location: { country, city },
timestamp: Date.now()
}),
{
headers: {
'content-type': 'application/json',
'cache-control': 'public, s-maxage=60'
}
}
);
}
3. Streaming SSR
// React 18 Streaming with Suspense
import { Suspense } from 'react';
function ProductPage({ productId }) {
return (
<div>
{/* Critical content loads first */}
<ProductHero productId={productId} />
{/* Non-critical content streams in */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<RecommendedProducts productId={productId} />
</Suspense>
</div>
);
}
// Server component that streams data
async function ProductReviews({ productId }) {
// This will stream when data is ready
const reviews = await fetch(`/api/products/${productId}/reviews`)
.then(res => res.json());
return (
<div>
{reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</div>
);
}
🔧 Optimization Techniques
1. Code Splitting Strategies
// Route-based code splitting
const HomePage = lazy(() => import('./pages/Home'));
const ProductPage = lazy(() => import('./pages/Product'));
const CheckoutPage = lazy(() => import('./pages/Checkout'));
// Component-based code splitting
const HeavyChart = lazy(() =>
import('./components/Chart').then(module => ({
default: module.HeavyChart
}))
);
// Conditional loading
const AdminPanel = lazy(() => {
if (user.role !== 'admin') {
return Promise.resolve({ default: () => <div>Access Denied</div> });
}
return import('./components/AdminPanel');
});
// Preloading for better UX
function ProductCard({ product }) {
const handleMouseEnter = () => {
// Preload product page when user hovers
import('./pages/Product');
};
return (
<div onMouseEnter={handleMouseEnter}>
<Link to={`/products/${product.id}`}>
{product.name}
</Link>
</div>
);
}
2. Caching Strategies
// Multi-layer caching strategy
const cachingStrategy = {
// CDN caching
cdn: {
static: '1y', // Static assets
pages: '1h', // HTML pages
api: '5m' // API responses
},
// Browser caching
browser: {
immutable: 'max-age=31536000, immutable',
shortTerm: 'max-age=300, must-revalidate',
noCache: 'no-cache, no-store, must-revalidate'
},
// Server caching
server: {
redis: 3600, // 1 hour
memory: 300, // 5 minutes
database: 86400 // 24 hours
}
};
// Implementation with Next.js
export async function getStaticProps() {
const cacheKey = `products-${Date.now()}`;
// Try cache first
let products = await redis.get(cacheKey);
if (!products) {
products = await fetch('/api/products').then(res => res.json());
await redis.setex(cacheKey, 3600, JSON.stringify(products));
}
return {
props: { products: JSON.parse(products) },
revalidate: 3600,
};
}
3. Image Optimization
// Next.js Image optimization
import Image from 'next/image';
function ProductGallery({ images }) {
return (
<div>
{/* Priority loading for above-the-fold images */}
<Image
src={images[0].src}
alt={images[0].alt}
width={800}
height={600}
priority
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
{/* Lazy loading for other images */}
{images.slice(1).map((image, index) => (
<Image
key={index}
src={image.src}
alt={image.alt}
width={400}
height={300}
loading="lazy"
sizes="(max-width: 768px) 100vw, 50vw"
/>
))}
</div>
);
}
// Astro Image optimization
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<!-- Automatic optimization -->
<Image
src={heroImage}
alt="Hero image"
width={1200}
height={600}
format="webp"
quality={80}
/>
📈 SEO Optimization
1. Meta Tags dan Structured Data
// Next.js SEO optimization
import Head from 'next/head';
function ProductPage({ product }) {
const structuredData = {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"description": product.description,
"image": product.images,
"offers": {
"@type": "Offer",
"price": product.price,
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
};
return (
<>
<Head>
<title>{product.name} | Your Store</title>
<meta name="description" content={product.description} />
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.image} />
<meta property="og:type" content="product" />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
</Head>
<ProductDetails product={product} />
</>
);
}
2. Core Web Vitals Optimization
// Performance monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics service
gtag('event', metric.name, {
value: Math.round(metric.value),
event_label: metric.id,
});
}
// Monitor all Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
// Optimize LCP
function optimizeLCP() {
// Preload critical resources
const link = document.createElement('link');
link.rel = 'preload';
link.href = '/hero-image.webp';
link.as = 'image';
document.head.appendChild(link);
}
// Optimize CLS
function optimizeCLS() {
// Reserve space for dynamic content
const placeholder = document.createElement('div');
placeholder.style.height = '200px';
placeholder.style.backgroundColor = '#f0f0f0';
document.getElementById('dynamic-content').appendChild(placeholder);
}
🚀 Deployment Strategies
1. Multi-Environment Setup
// Environment-specific configurations
const deploymentConfig = {
development: {
rendering: 'SSR',
caching: false,
analytics: false,
debugging: true
},
staging: {
rendering: 'SSG',
caching: true,
analytics: true,
debugging: true
},
production: {
rendering: 'Hybrid',
caching: true,
analytics: true,
debugging: false,
cdn: true,
compression: true
}
};
// Next.js configuration
module.exports = {
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
// Environment-specific settings
...(process.env.NODE_ENV === 'production' && {
compiler: {
removeConsole: true,
},
experimental: {
optimizeCss: true,
},
}),
// Image optimization
images: {
domains: ['example.com', 'cdn.example.com'],
formats: ['image/webp', 'image/avif'],
},
// Headers for security and performance
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
],
},
];
},
};
2. CI/CD Pipeline
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build application
run: npm run build
env:
NODE_ENV: production
API_URL: ${{ secrets.API_URL }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
🎯 Decision Framework
Choosing the Right Strategy
const decisionMatrix = {
// Content-heavy sites
blog: {
recommended: 'SSG',
framework: 'Astro',
reasoning: 'Static content, excellent SEO, fast loading'
},
// E-commerce
ecommerce: {
recommended: 'Hybrid (SSG + SSR)',
framework: 'Next.js',
reasoning: 'Product pages static, user pages dynamic'
},
// SaaS applications
saas: {
recommended: 'SSR',
framework: 'Nuxt.js',
reasoning: 'Personalized content, real-time data'
},
// Marketing sites
marketing: {
recommended: 'SSG',
framework: 'Astro',
reasoning: 'Static content, performance critical'
},
// Social platforms
social: {
recommended: 'SSR + CSR',
framework: 'Next.js',
reasoning: 'Real-time updates, personalization'
}
};
// Decision helper function
function chooseRenderingStrategy(requirements) {
const {
contentType,
updateFrequency,
personalization,
seoImportance,
interactivity,
scalability
} = requirements;
let score = {
SSG: 0,
SSR: 0,
CSR: 0
};
// Content type scoring
if (contentType === 'static') score.SSG += 3;
if (contentType === 'dynamic') score.SSR += 3;
if (contentType === 'interactive') score.CSR += 3;
// Update frequency
if (updateFrequency === 'rarely') score.SSG += 2;
if (updateFrequency === 'frequently') score.SSR += 2;
if (updateFrequency === 'realtime') score.CSR += 2;
// SEO importance
if (seoImportance === 'critical') {
score.SSG += 3;
score.SSR += 2;
}
// Personalization needs
if (personalization === 'high') {
score.SSR += 3;
score.CSR += 2;
}
// Return recommendation
const maxScore = Math.max(...Object.values(score));
const recommendation = Object.keys(score).find(key => score[key] === maxScore);
return {
recommendation,
scores: score,
reasoning: generateReasoning(recommendation, requirements)
};
}
🔮 Future Trends 2025
1. Edge Computing Evolution
// Edge-first architecture
const edgeStrategy = {
// Compute at the edge
userLocation