We use cookies to enhance your experience on the site
CodeWorlds

Rendering Strategies in Next.js App Router

Next.js revolutionized the way React applications are built, offering different rendering strategies that can be chosen depending on the page characteristics and project requirements. In this module, we will discuss how rendering strategies work in the context of the App Router, introduced in Next.js 13 and developed in subsequent versions.

Evolution of Rendering in Next.js

Next.js has undergone a significant evolution in its approach to rendering. In versions before 13, we used special functions like

getStaticProps
,
getServerSideProps
, or
getStaticPaths
to determine the rendering strategy for a given page. Each page had to choose one specific strategy.

In the App Router, the approach is different - rendering is now more granular and based on the concept of React Server Components. Instead of determining a strategy for the entire page, we can mix different approaches at the level of individual components.

React Server Components - The Foundation of App Router

At the core of the new rendering model are React Server Components (RSC), which have changed the way we think about React applications. The main features of RSC are:

  1. Server-side rendering - components are rendered on the server, and the client receives ready-made HTML
  2. Not included in the JavaScript bundle - RSC code is not sent to the browser
  3. Direct access to server resources - databases, file system, etc.
  4. No access to browser APIs - cannot use hooks or handle events

Rendering Strategies in App Router

In the App Router, we can still use traditional rendering strategies, but the way they are implemented is different. Let's look at each one.

1. Server-Side Rendering (SSR)

In the App Router, SSR is the default behavior for Route Handlers and Server Components.

1// app/products/page.tsx
2import { Product } from '@/components/Product';
3
4async function getData() {
5  // This function runs for every request on the server
6  const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
7
8  if (!res.ok) {
9    throw new Error('Failed to fetch data');
10  }
11
12  return res.json();
13}
14
15export default async function ProductsPage() {
16  const products = await getData();
17
18  return (
19    <div>
20      <h1>Our Products</h1>
21      <div className="grid">
22        {products.map(product => (
23          <Product key={product.id} data={product} />
24        ))}
25      </div>
26    </div>
27  );
28}

Key features of SSR in App Router:

  • We use the
    cache: 'no-store'
    option in the
    fetch
    function or
    revalidate: 0
    to indicate that data should be fetched on every request
  • Components are asynchronous (
    async
    ), which allows direct use of
    await
    in the component body
  • Every request to the page causes the page to be re-rendered on the server

Advantages of SSR:

  • Always up-to-date data
  • Better SEO performance (search engines see the full page content)
  • Ability to personalize content for each user

Disadvantages of SSR:

  • Slower page loading (server must process each request)
  • Higher server load

2. Static Site Generation (SSG)

In the App Router, SSG is achieved through the default behavior of

fetch
without the
cache: 'no-store'
option.

1// app/blog/page.tsx
2import { BlogPost } from '@/components/BlogPost';
3
4async function getData() {
5  // This function runs only at build time (or on demand in dev mode)
6  const res = await fetch('https://api.example.com/blog-posts');
7
8  if (!res.ok) {
9    throw new Error('Failed to fetch data');
10  }
11
12  return res.json();
13}
14
15export default async function BlogPage() {
16  const posts = await getData();
17
18  return (
19    <div>
20      <h1>Our Blog</h1>
21      <div className="grid">
22        {posts.map(post => (
23          <BlogPost key={post.id} data={post} />
24        ))}
25      </div>
26    </div>
27  );
28}

Key features of SSG in App Router:

  • By default, all components are rendered statically unless otherwise specified
  • Data is fetched at build time and "frozen" in static HTML
  • There is no need to use a special function like
    getStaticProps
    - a regular
    fetch
    is sufficient

Advantages of SSG:

  • Very fast page loading (static HTML files are served)
  • Minimal server load
  • Good SEO performance

Disadvantages of SSG:

  • Data may be outdated (updated only during rebuilds)
  • Not suitable for pages with user-personalized content

3. Incremental Static Regeneration (ISR)

In the App Router, ISR is achieved by adding the

revalidate
option to the
fetch
function or to the route segment.

1// app/products/[id]/page.tsx
2import { ProductDetails } from '@/components/ProductDetails';
3import { notFound } from 'next/navigation';
4
5// Option 1: Revalidation at the page level
6export const revalidate = 3600; // Revalidation every hour
7
8async function getProduct(id) {
9  // Option 2: Revalidation at the fetch function level
10  const res = await fetch(`https://api.example.com/products/${id}`, {
11    next: { revalidate: 3600 } // Also every hour
12  });
13
14  if (!res.ok) {
15    return null;
16  }
17
18  return res.json();
19}
20
21export default async function ProductPage({ params }) {
22  const product = await getProduct((await params).id);
23
24  if (!product) {
25    notFound();
26  }
27
28  return <ProductDetails product={product} />;
29}

Key features of ISR in App Router:

  • We can specify the revalidation time at the component level using an exported
    revalidate
    variable
  • We can specify the revalidation time at the
    fetch
    request level using the
    next: { revalidate: seconds }
    option
  • There is an option to force revalidation through an API Route Handler

Advantages of ISR:

  • Combines the advantages of SSG (speed) and SSR (relative data freshness)
  • Allows content updates without rebuilding the entire application
  • Good SEO support

Disadvantages of ISR:

  • Data is not 100% up-to-date (there is a delay depending on the set revalidation time)
  • More complex implementation and debugging

4. Client-Side Rendering (CSR)

In the App Router, CSR is achieved by marking a component as a "Client Component" using the

'use client'
directive at the beginning of the file.

1'use client';
2
3// app/dashboard/page.tsx
4import { useState, useEffect } from 'react';
5
6export default function Dashboard() {
7  const [data, setData] = useState(null);
8  const [isLoading, setIsLoading] = useState(true);
9
10  useEffect(() => {
11    async function fetchData() {
12      try {
13        const res = await fetch('/api/dashboard-data');
14        const jsonData = await res.json();
15        setData(jsonData);
16      } catch (error) {
17        console.error('Error fetching data:', error);
18      } finally {
19        setIsLoading(false);
20      }
21    }
22
23    fetchData();
24  }, []);
25
26  if (isLoading) return <div>Loading data...</div>;
27
28  return (
29    <div>
30      <h1>Control Panel</h1>
31      {/* Rendering data fetched on the client side */}
32      {data && data.map(item => (
33        <div key={item.id}>{item.name}</div>
34      ))}
35    </div>
36  );
37}

Key features of CSR in App Router:

  • The component must be marked with the
    'use client'
    directive
  • We can use React hooks (
    useState
    ,
    useEffect
    , etc.)
  • Data is fetched after the page loads in the browser

Advantages of CSR:

  • Can offer a better user experience for interactive applications
  • Lower server load (data is fetched directly from the API in the browser)
  • Good option for content personalized for a logged-in user

Disadvantages of CSR:

  • Weaker SEO performance (search engines may not see all content)
  • Slower initial content loading (user sees a loading state)
  • Larger JavaScript file size sent to the browser

Mixed Approach - Best Practice in App Router

One of the greatest advantages of the App Router is the ability to mix different rendering strategies within a single application, and even a single page. This allows us to choose the optimal strategy for each component.

Example of using mixed strategies:

1// app/products/[category]/page.tsx
2import { ProductList } from '@/components/ProductList';
3import { CategoryHeader } from '@/components/CategoryHeader';
4import FilterSidebar from '@/components/FilterSidebar'; // Client Component
5
6// This page and its data are rendered statically
7// with revalidation every hour
8export const revalidate = 3600;
9
10// We specify which categories should be generated statically
11export async function generateStaticParams() {
12  const categories = await fetch('https://api.example.com/categories').then(res => res.json());
13
14  return categories.map(category => ({
15    category: category.slug,
16  }));
17}
18
19async function getCategoryData(category) {
20  const categoryData = await fetch(`https://api.example.com/categories/${category}`);
21  return categoryData.json();
22}
23
24async function getProducts(category) {
25  const products = await fetch(`https://api.example.com/products?category=${category}`);
26  return products.json();
27}
28
29export default async function CategoryPage({ params }) {
30  // Fetch data in parallel
31  const [categoryData, products] = await Promise.all([
32    getCategoryData((await params).category),
33    getProducts((await params).category),
34  ]);
35
36  return (
37    <div className="grid grid-cols-4 gap-4">
38      {/* Statically rendered category header */}
39      <CategoryHeader data={categoryData} />
40
41      {/* Interactive filter rendered on the client side */}
42      <div className="col-span-1">
43        <FilterSidebar initialProducts={products} />
44      </div>
45
46      {/* Statically rendered product list */}
47      <div className="col-span-3">
48        <ProductList products={products} />
49      </div>
50    </div>
51  );
52}

In this example:

  • The entire page is rendered statically with hourly revalidation (ISR)
  • Only specific categories are generated at build time (
    generateStaticParams
    )
  • The
    FilterSidebar
    component is a Client Component, enabling interactive client-side filtering
  • The
    CategoryHeader
    and
    ProductList
    components are Server Components rendered statically

When to Use Which Strategy?

The choice of rendering strategy depends on the characteristics of the given part of the application:

  1. Static pages (documentation, landing pages, informational pages):

    • Use SSG (default behavior)
  2. Dynamic content that doesn't require the latest data (blog, product catalog):

    • Use ISR with an appropriate revalidation time
  3. Content requiring the latest data (stock prices, news, sports results):

    • Use SSR (
      cache: 'no-store'
      )
  4. Personalized content and interactive UI elements (dashboards, forms):

    • Use CSR (
      'use client'
      )
  5. Hybrid approach (e.g., e-commerce):

    • Static page skeleton (SSG/ISR)
    • Dynamic product data (SSR)
    • Interactive elements like cart, filters (CSR)

Dynamic and Static Routes

In the App Router, we can also specify which dynamic paths should be generated statically during the application build:

1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
4
5  return posts.map((post) => ({
6    slug: post.slug,
7  }));
8}

For routes not covered by

generateStaticParams
:

  • If we set
    dynamicParams = false
    in the segment options, Next.js will return a 404
  • If
    dynamicParams = true
    (default) or is not set, Next.js will generate the page on demand (SSR)

Optimizing Dynamic Pages

If we have many dynamic subpages (e.g., thousands of products in e-commerce), we can optimize the build process by:

  1. Generating the most popular pages statically during the build:
1export async function generateStaticParams() {
2  // Fetch only the most popular products
3  const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json());
4
5  return popularProducts.map((product) => ({
6    id: product.id,
7  }));
8}
  1. On-demand Revalidation - revalidating individual pages on demand, e.g., after a product update:
1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6  const { path, secret } = await request.json();
7
8  // Checking the secret key for security
9  if (secret !== process.env.REVALIDATION_SECRET) {
10    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
11  }
12
13  if (!path) {
14    return NextResponse.json({ message: 'Path is required' }, { status: 400 });
15  }
16
17  try {
18    // Revalidating the specified path
19    revalidatePath(path);
20    return NextResponse.json({ revalidated: true, message: `Revalidated ${path}` });
21  } catch (err) {
22    return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
23  }
24}

Comparison of Rendering Strategies

| Feature | SSG | ISR | SSR | CSR | |---------|-----|-----|-----|-----| | Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | | SEO | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | | Data freshness | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | Server load | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | | Interactivity | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |

Summary

The App Router in Next.js offers advanced capabilities for mixing different rendering strategies at the component level, which enables creating efficient and flexible applications. The key to success is choosing the right strategy for each part of the application, considering performance, SEO, and data freshness requirements.

In the next module, we will discuss in more detail the data revalidation mechanisms in Next.js App Router, which are crucial for implementing ISR and ensuring content freshness.

Go to CodeWorlds