SSG• Concept guide built at build time for instant access

Hybrid Rendering Concepts

This demo showcases modern web rendering strategies using Next.js 14+. Each approach has unique benefits and use cases. Explore the concepts below to understand when and why to use each strategy.

SSG

Static Site Generation (SSG)

Pages are pre-rendered at build time, resulting in the fastest possible loading experience.

How it works:

  • Pages are generated during the build process
  • HTML, CSS, and JavaScript are pre-computed
  • Served directly from CDN with minimal server processing
What Makes a Page SSG in Next.jstypescript
// app/page.tsx
// āœ… SSG happens automatically when:
// 1. No request-specific data (no cookies, headers, searchParams)
// 2. All data can be fetched at build time
// 3. No dynamic segments without generateStaticParams

export default async function Home() {
  // This runs at BUILD TIME, not request time
  const [recipes, categories] = await Promise.all([
    getRecipes(),      // Static data from JSON/database
    getCategories()    // Available at build time
  ]);

  const featuredRecipes = recipes.slice(0, 3);

  return (
    <div>
      <h1>Featured Recipes</h1>
      {featuredRecipes.map((recipe) => (
        <RecipeCard key={recipe.id} recipe={recipe} />
      ))}
    </div>
  );
}

// 🚫 This would force SSR instead:
// export default async function Home({ searchParams }) {
//   const query = searchParams.q; // Request-specific data = SSR
//   const recipes = await searchRecipes(query);
//   return <div>...</div>;
// }
šŸ”‘ What Triggers SSG in Next.js:
āœ… SSG Automatically:
  • • No request-specific data
  • • No searchParams, cookies, headers
  • • Static data fetching only
  • • generateStaticParams for dynamic routes
āŒ Forces SSR Instead:
  • • Using searchParams
  • • Reading cookies() or headers()
  • • Dynamic data per request
  • • No generateStaticParams for [dynamic]
āœ… Best for:
  • • Landing pages
  • • Marketing content
  • • Blog posts
  • • Product catalogs
  • • Documentation
āŒ Not ideal for:
  • • Frequently changing data
  • • User-specific content
  • • Real-time features
  • • Dynamic interactions
SSR

Server-Side Rendering (SSR)

Pages are rendered on the server for each request, ensuring fresh data and optimal SEO.

How it works:

  • Server renders HTML for each incoming request
  • Fresh data is fetched on every request
  • Client receives fully rendered HTML
Recipe Comments (SSR)typescript
// Comments are server-rendered for fresh data
export default async function RecipePage({ params }: RecipePageProps) {
  const [recipe, comments] = await Promise.all([
    getRecipe(params.id),
    getComments(params.id) // Fresh comments on every request
  ]);

  return (
    <section>
      <h2>Reviews ({comments.length})</h2>
      {comments.map((comment) => (
        <div key={comment.id}>
          <div className="font-semibold">{comment.author}</div>
          <p>{comment.content}</p>
          <time className="text-gray-500">
            {new Date(comment.createdAt).toLocaleDateString()}
          </time>
        </div>
      ))}
    </section>
  );
}
āœ… Best for:
  • • Dynamic content
  • • User-specific data
  • • SEO-critical pages
  • • Fresh data requirements
  • • Social media shares
āŒ Trade-offs:
  • • Slower than SSG
  • • Server processing required
  • • Higher server costs
  • • TTFB (Time to First Byte) delay
CSR

Client-Side Rendering (CSR)

JavaScript runs in the browser to render content dynamically, enabling rich interactions.

How it works:

  • Initial HTML shell is minimal
  • JavaScript renders content in the browser
  • API calls fetch data after page load
Search Page Implementationtypescript
'use client';

export default function SearchPage() {
  const [query, setQuery] = useState('');
  const [recipes, setRecipes] = useState<Recipe[]>([]);
  const [loading, setLoading] = useState(false);

  const searchRecipes = useCallback(async (searchQuery: string) => {
    setLoading(true);
    try {
      const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
      const data = await response.json();
      setRecipes(data.recipes || []);
    } finally {
      setLoading(false);
    }
  }, []);

  // Debounced search
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      searchRecipes(query);
    }, 300);
    return () => clearTimeout(timeoutId);
  }, [query, searchRecipes]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search recipes..."
        className="w-full px-4 py-2 border rounded-lg"
      />
      {loading ? <Spinner /> : <RecipeList recipes={recipes} />}
    </div>
  );
}
āœ… Best for:
  • • Interactive features
  • • Real-time updates
  • • User interactions
  • • Dynamic filtering
  • • Complex state management
āŒ Trade-offs:
  • • Poor initial SEO
  • • Slower first contentful paint
  • • JavaScript dependency
  • • Bundle size impact
ISR

Incremental Static Regeneration (ISR)

Combines the benefits of SSG with the ability to update static content after deployment.

How it works:

  • Pages are initially generated at build time
  • Background regeneration occurs based on revalidation rules
  • Users always get fast static pages, with updates seamlessly applied
Recipe Details with ISRtypescript
// šŸ”‘ KEY: This export enables ISR
export const revalidate = 3600; // Revalidate every hour

// Generate static params for all recipe pages
export async function generateStaticParams() {
  const recipes = await getRecipes();
  return recipes.map((recipe) => ({
    id: recipe.id,  // Pre-generate /recipe/spaghetti-carbonara, etc.
  }));
}

export default async function RecipePage({ params }: RecipePageProps) {
  // This runs at build time AND periodically after deployment
  const recipe = await getRecipe(params.id);
  
  return (
    <article>
      <h1>{recipe.title}</h1>
      <p>{recipe.description}</p>
      <div className="ingredients">
        {recipe.ingredients.map((ingredient, i) => (
          <div key={`ingredient-${i}`}>{ingredient}</div>
        ))}
      </div>
    </article>
  );
}

// šŸŽÆ What triggers ISR:
// āœ… export const revalidate = number
// āœ… generateStaticParams for dynamic routes  
// āœ… No request-specific data (like searchParams)
ISR Revalidation Strategies:
Time-based:
export const revalidate = 3600

Regenerate every hour

On-demand:
revalidatePath('/recipes/id')

Trigger updates manually

āœ… Best for:
  • • E-commerce product pages
  • • Blog posts with comments
  • • News articles
  • • Content that updates periodically
  • • High-traffic pages
āš–ļø Considerations:
  • • Complexity in cache management
  • • Potential stale content windows
  • • Build time requirements
  • • Revalidation strategy planning

šŸ’§ Understanding Hydration

Hydration is the process where React takes over static HTML from the server and makes it interactive. It's the bridge between server-rendered content and client-side interactivity.

1
Server Renders HTML

The server generates complete HTML with all content, but without JavaScript event handlers.

<button>Click me</button>
↑ Static HTML (not interactive yet)

2
React Hydrates

React JavaScript loads and "hydrates" the static HTML, attaching event listeners and state.

<button onClick={handleClick}>Click me</button>
↑ Now interactive!

3
Seamless Transition

Users see content immediately (server HTML) while JavaScript loads in the background, then interactivity is progressively enhanced.

Hydration in Actiontypescript
// This component demonstrates hydration
'use client';

import { useState, useEffect } from 'react';

export function HydrationDemo() {
  const [isHydrated, setIsHydrated] = useState(false);
  const [count, setCount] = useState(0);

  // This effect only runs on the client after hydration
  useEffect(() => {
    setIsHydrated(true);
  }, []);

  return (
    <div className="p-4 border rounded">
      <h3>Hydration Status</h3>
      <p>
        Status: {isHydrated ? 'āœ… Hydrated' : 'ā³ Server HTML'}
      </p>
      
      {/* This button starts non-interactive */}
      <button 
        onClick={() => setCount(count + 1)}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        Count: {count}
      </button>
      
      {/* This only appears after hydration */}
      {isHydrated && (
        <p className="text-green-600 text-sm mt-2">
          šŸŽ‰ JavaScript is now active!
        </p>
      )}
    </div>
  );
}

Common Hydration Issues

Hydration Mismatch: Server and client render different content
Flash of Unstyled Content: Styles not applied during hydration
Slow Hydration: Large JavaScript bundles delay interactivity

Hydration Timeline Visualization

Page LoadHydration Complete
HTML Visible
~100ms
JS Downloads
~500ms
Interactive
~800ms

Try It Yourself

šŸ’§ Live Hydration Demo

ā³ Server HTML

Button not yet interactive...

Hydration timestamp:
Not hydrated yet...
Progressive Enhancement:
HTML content visible immediately
JavaScript interactivity loaded
User interaction detected

Try this: Disable JavaScript in your browser and reload. You'll see the HTML content but no interactivity.

šŸ“– Learn more about Hydration in Next.js

šŸļø Advanced Rendering Concepts

Modern web development continues to evolve with new patterns like Islands Architecture and Streaming SSR that push the boundaries of performance and user experience.

šŸļø

Islands Architecture

Better implemented in Astro, Fresh, or Qwik

Islands of interactivity in a sea of static HTML. Only specific components are hydrated, dramatically reducing JavaScript bundle size and improving performance.

How Islands Work:

  • • Static HTML by default
  • • Selective hydration of interactive components
  • • Components load independently
  • • Minimal JavaScript footprint
Islands Architecture Concepttypescript
<!-- Astro Example (Islands Architecture) -->
---
// This runs on the server only
const staticData = await fetchData();
---

<Layout>
  <!-- Static HTML - no JS needed -->
  <Header />
  <StaticContent data={staticData} />
  
  <!-- Interactive island - hydrated on client -->
  <SearchBox client:load />
  
  <!-- Another island - lazy loaded -->
  <CommentSection client:visible />
  
  <!-- Static footer - no JS -->
  <Footer />
</Layout>

<!-- Result: Only SearchBox and CommentSection 
     have JavaScript, rest is pure HTML -->

Next.js vs Islands:

Next.js hydrates entire pages by default. While you can optimize with dynamic imports and React.lazy, true islands architecture requires frameworks specifically designed for it like Astro or Qwik.

🌊

Streaming SSR

Available in Next.js App Router

Stream HTML to the browser as it's generated, showing content progressively instead of waiting for the entire page to render.

Streaming Benefits:

  • • First content appears faster
  • • Better perceived performance
  • • Handles slow data sources gracefully
  • • Reduces Time to First Byte (TTFB)
Streaming with Suspensetypescript
import { Suspense } from 'react';

export default async function RecipePage({ params }) {
  // Fast data loads immediately
  const recipe = await getRecipe(params.id);

  return (
    <div>
      {/* This renders and streams first */}
      <h1>{recipe.title}</h1>
      <img src={recipe.image} alt={recipe.title} />
      
      {/* This streams when ready */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments recipeId={params.id} />
      </Suspense>
      
      {/* This also streams independently */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedRecipes category={recipe.category} />
      </Suspense>
    </div>
  );
}

// This component streams when data is ready
async function Comments({ recipeId }) {
  // This might be slow (database query, API call)
  const comments = await getComments(recipeId);
  
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>{comment.content}</div>
      ))}
    </div>
  );
}

In This Demo:

Visit any recipe page to see streaming in action. The recipe content loads first, then comments stream in when ready!

Framework Comparison for Advanced Patterns

FrameworkIslandsStreamingBundle SizeBest For
Next.jsPartial
Dynamic imports
Excellent
Built-in Suspense
Medium
Full hydration
Full-stack apps
AstroExcellent
Native islands
Limited
Static focus
Minimal
Selective JS
Content sites
QwikExcellent
Resumability
Good
Progressive
Minimal
Fine-grained
Interactive apps
Fresh (Deno)Excellent
Island-first
Good
Edge focus
Minimal
No build step
Edge apps

Next.js Core Concepts

Beyond rendering strategies, Next.js provides a rich ecosystem of features. Here are key concepts that power modern web applications.

šŸ“
App Router

File-system based routing with layouts, nested routes, and parallel routes. Replaces the Pages Router with more powerful patterns.

Used in this demo:
• app/page.tsx (homepage)
• app/recipes/[id]/page.tsx
• app/layout.tsx (shared layout)
Learn about App Router →

āš›ļø
Server Components

React components that render on the server, reducing client bundle size and enabling direct database access.

Benefits:
• Zero client-side JavaScript
• Direct database queries
• Automatic code splitting
Learn about Server Components →

šŸŽ¬
Streaming & Suspense

Progressive page loading that streams content as it becomes available, improving perceived performance.

Features:
• loading.tsx files
• React Suspense boundaries
• Parallel data fetching
Learn about Streaming →

šŸ› ļø
Developer Experience

Built-in tools for debugging, optimization, and development workflow that make building web apps faster and more enjoyable.

DevTools:
• Fast Refresh (instant updates)
• Error overlay with stack traces
• Performance insights
Learn about Optimization →

šŸ–¼ļø
Built-in Optimizations

Automatic optimizations for images, fonts, scripts, and more. Performance best practices applied by default.

Auto-optimized:
• Image optimization & lazy loading
• Font optimization
• Bundle splitting & tree shaking
Learn about Image Optimization →

šŸš€
Deployment & Scaling

Seamless deployment with Vercel, edge functions, and automatic scaling. Built for production from day one.

Production features:
• Edge Runtime support
• Automatic static optimization
• Built-in analytics
Learn about Deployment →
App Router File Structuretypescript
app/
ā”œā”€ā”€ layout.tsx          // Root layout (shared across all pages)
ā”œā”€ā”€ page.tsx           // Homepage (/)
ā”œā”€ā”€ loading.tsx        // Loading UI for this level
ā”œā”€ā”€ error.tsx          // Error UI for this level
ā”œā”€ā”€ not-found.tsx      // 404 page
ā”œā”€ā”€ recipes/
│   ā”œā”€ā”€ layout.tsx     // Recipes layout
│   ā”œā”€ā”€ page.tsx       // Recipes list (/recipes)
│   └── [id]/
│       ā”œā”€ā”€ page.tsx   // Recipe detail (/recipes/[id])
│       └── loading.tsx // Loading UI for recipe details
└── api/
    └── search/
        └── route.ts   // API endpoint (/api/search)

Performance Comparison

StrategyTTFBFCPSEOFreshnessScalability
SSG
Excellent
Excellent
Excellent
Poor
Excellent
SSR
Good
Good
Excellent
Excellent
Poor
CSR
Excellent
Poor
Poor
Excellent
Good
ISR
Excellent
Excellent
Excellent
Good
Excellent

TTFB: Time to First Byte | FCP: First Contentful Paint | SEO: Search Engine Optimization

Ready to Explore?

Now that you understand the concepts, explore different pages in this demo to see each rendering strategy in action.