Rendering Strategy và Best Practices

Bài 13 – Chiến lược render tối ưu và thực tiễn áp dụng trong Next.js

18/11/2025 DaiPhan
Bài 13 / 18

1. Giới thiệu về Rendering Strategy

Next.js cung cấp nhiều cơ chế render: CSR (Client-Side Rendering), SSR (Server-Side Rendering), SSG (Static Site Generation), ISR (Incremental Static Regeneration)Server Components. Việc lựa chọn sai chiến lược render có thể:

  • Làm giảm hiệu năng trang web
  • Tăng chi phí server và hosting
  • Ảnh hưởng đến SEO và user experience
  • Gây chậm thời gian tải trang

Bài học này hướng dẫn cách chọn phương pháp render đúng cho từng loại trang và use case cụ thể.

2. Nội dung chính

2.1 Mô hình render phổ biến trong Next.js

StrategyThời điểm renderUse casePerformanceSEO
SSGBuild timeContent tĩnh⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
ISRBuild + RuntimeContent thay đổi định kỳ⭐⭐⭐⭐⭐⭐⭐⭐⭐
SSRRequest timeData realtime, Personalized⭐⭐⭐⭐⭐⭐⭐
CSRBrowserInteractive apps⭐⭐⭐⭐
Server ComponentsServerMixed content⭐⭐⭐⭐⭐⭐⭐⭐⭐

2.2 Khi nào dùng CSR / SSR / SSG / ISR

SSG (Static Site Generation)

  • ✅ Blog posts, marketing pages, documentation
  • ✅ Content không thường xuyên thay đổi
  • ✅ Cần performance tốt nhất
  • ❌ Không phù hợp cho user-specific content

ISR (Incremental Static Regeneration)

  • ✅ News websites, e-commerce catalogs
  • ✅ Content cập nhật định kỳ (vài phút/giờ)
  • ✅ Cần balance giữa performance và freshness
  • ❌ Không cho realtime data

SSR (Server-Side Rendering)

  • ✅ User dashboards, admin panels
  • ✅ Data phụ thuộc cookies/sessions
  • ✅ Realtime data (stock prices, chat)
  • ❌ Tốn server resources

CSR (Client-Side Rendering)

  • ✅ Interactive applications (games, tools)
  • ✅ Data không cần SEO (dashboards nội bộ)
  • ✅ Heavy client-side interactions
  • ❌ Không tốt cho SEO ban đầu

2.3 Tối ưu bằng Server Components

Server Components cho phép bạn kết hợp server-rendered và client-rendered content trong cùng một trang:

// app/products/page.tsx (Server Component)
import { Suspense } from 'react';
import { ProductList } from './product-list';
import { FilterSidebar } from './filter-sidebar';

export default async function ProductsPage() {
  // Server-side data fetching
  const categories = await getCategories();
  
  return (
    <div className="grid grid-cols-4 gap-6">
      {/* Server Component - Static */}
      <aside className="col-span-1">
        <FilterSidebar categories={categories} />
      </aside>
      
      {/* Mixed content */}
      <main className="col-span-3">
        <Suspense fallback={<ProductListSkeleton />}>
          <ProductList />
        </Suspense>
      </main>
    </div>
  );
}

2.4 Chiến lược caching chuẩn trong App Router

// 1. Default cache (SSG)
const response = await fetch('https://api.example.com/posts');

// 2. ISR với revalidate
const response = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 } // 1 giờ
});

// 3. No cache (SSR)
const response = await fetch('https://api.example.com/posts', {
  cache: 'no-store'
});

// 4. Cache với tags (On-demand revalidation)
const response = await fetch('https://api.example.com/posts', {
  next: { 
    revalidate: 3600,
    tags: ['posts', 'blog']
  }
});

2.5 Quy tắc lựa chọn render cho production

Quy tắc 1: Content-first approach

Content Type → Rendering Strategy
├── Static Content (blog, docs) → SSG
├── Semi-static (news, products) → ISR  
├── Dynamic Content (user data) → SSR
└── Interactive Only (games, tools) → CSR

Quy tắc 2: Performance budget

// Target metrics
const PERFORMANCE_BUDGET = {
  TTFB: '< 200ms',      // Time to First Byte
  FCP: '< 1.5s',        // First Contentful Paint  
  LCP: '< 2.5s',        // Largest Contentful Paint
  TTI: '< 3.5s'         // Time to Interactive
};

Quy tắc 3: Hybrid approach

  • Sử dụng Server Components cho phần tĩnh
  • Client Components cho phần tương tác
  • Progressive enhancement cho CSR

3. Ví dụ thực tế

3.1 E-commerce Platform - Multi-strategy Approach

// app/page.tsx - Homepage (SSG)
export default async function Homepage() {
  // Static content - SSG by default
  const featuredProducts = await fetch('https://api.example.com/featured-products');
  const testimonials = await fetch('https://api.example.com/testimonials');
  
  return (
    <div>
      <Hero />
      <FeaturedProducts products={featuredProducts} />
      <Testimonials data={testimonials} />
    </div>
  );
}

// app/products/[id]/page.tsx - Product Detail (ISR)
export const revalidate = 3600; // 1 hour

export default async function ProductDetail({ params }: { params: { id: string } }) {
  // Product info changes occasionally
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 3600 }
  });
  
  return <ProductPage product={product} />;
}

// app/cart/page.tsx - Shopping Cart (SSR)
export default async function CartPage() {
  // Need cookies for session/auth
  const cookieStore = cookies();
  const sessionId = cookieStore.get('session-id');
  
  // Real-time inventory check
  const cartData = await fetch('https://api.example.com/cart', {
    headers: { 'Cookie': `session-id=${sessionId}` },
    cache: 'no-store'
  });
  
  return <ShoppingCart data={cartData} />;
}

// components/add-to-cart.tsx - Interactive Component (CSR)
'use client';

import { useState } from 'react';
import { useCart } from '@/hooks/use-cart';

export function AddToCart({ productId }: { productId: string }) {
  const [isAdding, setIsAdding] = useState(false);
  const { addItem } = useCart();
  
  const handleAddToCart = async () => {
    setIsAdding(true);
    await addItem(productId);
    setIsAdding(false);
  };
  
  return (
    <button 
      onClick={handleAddToCart}
      disabled={isAdding}
      className="bg-blue-600 text-white px-6 py-2 rounded-lg"
    >
      {isAdding ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

3.2 News Website - Performance Focused

// app/news/page.tsx - News List (ISR)
export const revalidate = 300; // 5 minutes

export default async function NewsPage() {
  // News updates frequently but not real-time
  const news = await fetch('https://api.news.com/latest', {
    next: { revalidate: 300 }
  });
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {news.articles.map((article: any) => (
        <NewsCard key={article.id} article={article} />
      ))}
    </div>
  );
}

// app/news/[slug]/page.tsx - Article Detail (ISR with on-demand)
export async function generateStaticParams() {
  // Pre-render popular articles
  const popularArticles = await fetch('https://api.news.com/popular');
  return popularArticles.map((article: any) => ({
    slug: article.slug
  }));
}

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const article = await fetch(`https://api.news.com/articles/${params.slug}`, {
    next: { 
      revalidate: 600, // 10 minutes
      tags: ['articles', params.slug]
    }
  });
  
  if (!article) notFound();
  
  return <ArticleContent article={article} />;
}

// app/live/page.tsx - Live Updates (SSR)
export default async function LiveUpdatesPage() {
  // Breaking news needs to be fresh
  const breakingNews = await fetch('https://api.news.com/breaking', {
    cache: 'no-store'
  });
  
  return (
    <div>
      <h1>Live Updates</h1>
      <BreakingNewsTicker news={breakingNews} />
    </div>
  );
}

3.3 SaaS Dashboard - User Specific

// app/dashboard/page.tsx - Dashboard Layout (Server Component)
export default async function DashboardLayout() {
  // Server-side data that doesn't change often
  const user = await getCurrentUser();
  const sidebarItems = await getUserMenuItems(user.role);
  
  return (
    <div className="flex h-screen">
      <Sidebar items={sidebarItems} />
      <main className="flex-1 overflow-auto">
        <DashboardContent />
      </main>
    </div>
  );
}

// components/dashboard-stats.tsx - Interactive Stats (Client Component)
'use client';

import { useEffect, useState } from 'react';

export function DashboardStats() {
  const [stats, setStats] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Fetch real-time stats
    fetchStats().then(data => {
      setStats(data);
      setLoading(false);
    });
    
    // Update every 30 seconds
    const interval = setInterval(fetchStats, 30000);
    return () => clearInterval(interval);
  }, []);
  
  if (loading) return <StatsSkeleton />;
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
      <StatCard title="Revenue" value={stats.revenue} />
      <StatCard title="Users" value={stats.users} />
      <StatCard title="Orders" value={stats.orders} />
      <StatCard title="Conversion" value={stats.conversion} />
    </div>
  );
}

// app/dashboard/analytics/page.tsx - Analytics (SSR)
export default async function AnalyticsPage() {
  // Need fresh data for analytics
  const analytics = await fetchAnalytics({
    dateRange: 'last-30-days',
    metrics: ['revenue', 'users', 'conversion']
  }, {
    cache: 'no-store'
  });
  
  return (
    <div>
      <AnalyticsHeader />
      <AnalyticsCharts data={analytics} />
    </div>
  );
}

4. Kiến thức trọng tâm

4.1 Quy tắc vàng cho rendering strategy

1. Ưu tiên SSG & ISR cho các trang public

  • ✅ Tốc độ cực nhanh (pre-generated)
  • ✅ Giảm tải server (CDN-friendly)
  • ✅ SEO tốt nhất
  • ✅ Chi phí hosting thấp

2. Chỉ sử dụng SSR khi thực sự cần thiết

  • ✅ Dữ liệu realtime (stock prices, chat)
  • ✅ User-specific content (dashboard, profile)
  • ✅ Cần cookies/headers (authentication)
  • ❌ Tránh SSR cho content tĩnh

3. CSR cho UI tương tác mạnh

  • ✅ Interactive applications (games, tools)
  • ✅ Real-time updates (notifications, chat)
  • ✅ User preferences (theme, language)
  • ❌ Tránh CSR cho content cần SEO

4.2 Performance comparison

Strategy    TTFB    FCP     LCP     TTI     SEO     Cost
SSG         50ms    800ms   1.2s    1.5s    ⭐⭐⭐⭐⭐  $$$$
ISR         80ms    900ms   1.4s    1.7s    ⭐⭐⭐⭐⭐  $$$$
SSR         300ms   1.5s    2.1s    2.8s    ⭐⭐⭐⭐⭐  $$
CSR         200ms   1.2s    2.5s    3.2s    ⭐⭐      $$
Hybrid      100ms   900ms   1.3s    1.8s    ⭐⭐⭐⭐⭐  $$$$

4.3 Decision tree for rendering

Need SEO?
├── Yes
│   ├── Static content? → SSG
│   ├── Updates periodically? → ISR
│   └── Real-time data? → SSR
└── No
    ├── User interaction? → CSR
    └── Internal tool? → CSR/SSR

4.4 Common anti-patterns to avoid

// ❌ Don't: SSR for static content
export default async function AboutPage() {
  const team = await fetch('https://api.example.com/team'); // Static data
  return <TeamPage team={team} />;
}

// ✅ Do: Use SSG instead
export default async function AboutPage() {
  const team = await fetch('https://api.example.com/team');
  return <TeamPage team={team} />;
}
// Next.js will automatically use SSG

// ❌ Don't: CSR for SEO-critical content
'use client';
export default function ProductDescription({ id }) {
  const [product, setProduct] = useState(null);
  
  useEffect(() => {
    fetch(`/api/products/${id}`).then(res => res.json()).then(setProduct);
  }, [id]);
  
  return product ? <div>{product.description}</div> : null;
}

// ✅ Do: Use SSG/ISR for product pages
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`);
  return <ProductDetail product={product} />;
}

5. Bài tập nhanh

Bài 1: Phân tích và cải thiện performance

Trước (poor performance):

// app/products/page.tsx - Currently SSR
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products');
  return <ProductList products={products} />;
}

Yêu cầu:

  1. Chuyển sang ISR với revalidate 1 giờ
  2. Thêm loading skeleton
  3. Implement error handling
  4. Measure TTFB improvement

Sau (improved):

// Implement your solution here

Bài 2: Multi-strategy e-commerce

Tạo một e-commerce với các requirements:

Pages và strategy:
├── / (homepage) → SSG
├── /products → ISR (5 phút)
├── /products/[id] → ISR (1 giờ) + generateStaticParams
├── /cart → SSR (need cookies)
├── /checkout → SSR (secure)
└── /account → SSR (user-specific)

Bonus: Thêm client components cho:

  • Add to cart buttons
  • Cart counter
  • Search functionality

Bài 3: Performance monitoring

Setup monitoring cho các metrics:

// utils/performance.ts
export function measurePerformance() {
  // Measure TTFB, FCP, LCP, TTI
  // Send to analytics
  // Create performance dashboard
}

Metrics to track:

  • Time to First Byte (TTFB)
  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Time to Interactive (TTI)
  • Bundle size by page

6. Kết luận

Việc chọn đúng chiến lược render là một trong những kỹ năng quan trọng nhất trong Next.js development. Một lựa chọn đúng sẽ mang lại:

Lợi ích hiệu năng:

  • Tốc độ tải trang nhanh hơn 50-80%
  • SEO ranking cao hơn
  • User experience tốt hơn
  • Conversion rate cao hơn

Lợi ích kinh doanh:

  • Chi phí hosting giảm 60-90%
  • Server load giảm đáng kể
  • Scalability tốt hơn
  • Developer productivity cao hơn

Key takeaways:

  • Luôn bắt đầu với SSG và ISR cho public content
  • Chỉ dùng SSR khi thực sự cần realtime/personalized data
  • CSR cho interactive components, không phải toàn bộ page
  • Monitor performance metrics để validate choices
  • Review và adjust strategy khi requirements thay đổi

Remember: Rendering strategy không phải là one-size-fits-all. Hãy analyze your specific use case, performance requirements, và business goals để make informed decisions.

18 bài học
Bài 13
Tiến độ hoàn thành 72%

Đã hoàn thành 13/18 bài học