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.

20 menit baca Oleh Hilal Technologic
Server-Side Rendering vs Static Site Generation: Pilih Mana untuk Project 2025?

⚡ 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)

FrameworkBuild TimeBundle SizeFirst LoadHydrationSEO Score
Next.js 1445s85KB1.2s200ms98/100
Nuxt 338s92KB1.1s180ms97/100
Astro25s12KB0.8s0ms100/100
SvelteKit32s45KB0.9s150ms99/100
Gatsby120s78KB1.0s250ms96/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)
  };
}

1. Edge Computing Evolution

// Edge-first architecture
const edgeStrategy = {
  // Compute at the edge
  userLocation