20 June 2025

A Short Guide to Rendering in Next.js

Blazity

Performance Optimization

This article focuses on rendering strategies in Next.js, including Server-Side Rendering (SSR), Static Site Generation (SSG), Incremental Static Regeneration (ISR), Client-Side Rendering (CSR), and Partial Pre-rendering (PPR).

Blazity team
Blazity team
<h1>A Short Guide to Rendering in Next.js</h1>


The Expert Guide to Next.js Performance Optimization

This article is part of The Expert Guide to Next.js Performance Optimization, a comprehensive Ebook you can download for free here.


The guide covers every aspect of building fast, scalable web apps with Next.js, including:


- Code Splitting

- Streaming, Suspense & Hydration

- Next/Image Component

- Third-Party Scripts

- Font Optimization

- Rendering Strategies

- Core Web Vitals

- Infrastructure

- Real-life Examples

- How to Measure Web Performance

- Bundle Analysis.

The Expert Guide to Next.js Performance Optimization

This article provides an in-depth exploration of rendering strategies in Next.js, emphasizing their crucial role in web performance and user experience. It covers traditional methods like Server-Side Rendering (SSR), Static Site Generation (SSG), Incremental Static Regeneration (ISR), and Client-Side Rendering (CSR). Furthermore, it introduces newer paradigms within the App Router, such as React Server Components (RSC) and Partial Pre-rendering (PPR), highlighting their benefits in optimizing data fetching, reducing client-side JavaScript, and simplifying application complexity.

A Comprehensive Overview of Next.js Rendering Strategies

Rendering is a core part of web performance and directly affects how fast users can see and interact with content. In Next.js, choosing the right rendering strategy is essential for balancing speed, flexibility, and scalability.

The framework supports several rendering methods:

  • Server-Side Rendering (SSR),
  • Static Site Generation (SSG),
  • Incremental Static Regeneration (ISR),
  • and Client-Side Rendering (CSR).

The introduction of the App Router brings new paradigms like React Server Components (RSC), which improve performance by optimizing data fetching and reducing client-side JavaScript and application complexity. Gaining a solid understanding of how this works is essential.

Now, let’s dive into the implementation.

Implementation

In this chapter, we’ll explore:

  • how rendering strategies differ between the App Router and the Pages Router,
  • the key elements in both approaches,
  • how to decide which approach fits your use case,
  • and best practices to optimize performance.

We will do so using code implementation, as code says more than a thousand words.

App Router Rendering Strategies

Elements

Server Components

Server Components represent a fundamental shift in how we build React applications. Unlike traditional components that execute both on the server and client, Server Components run exclusively on the server, sending lightweight JSON representation or the rendered HTML to the client. This approach combines the best aspects of server-side rendering with the component-based architecture that makes React powerful.


Let's explore how Server Components work in detail.


Core Characteristics:


Server-Side Execution


Code

1// app/products/page.tsx
2async function ProductsPage() {
3  // This code only runs on the server
4  const products = await db.query('SELECT * FROM products');
5  return <ProductList products={products} />;
6}
  • All code executes in the server environment.
  • Direct database access without API layers.
  • Secure handling of sensitive data.
  • Reduced risk of exposing business logic.


Bundle Size Optimization

Code

1// This large dependency only exists on the server
2import { massiveLibrary } from 'huge-package';
3
4export default function DataVisualizer() {
5  const processedData = massiveLibrary.process(data);
6  return <div>{processedData}</div>;
7}
8
9
  • Dependencies don't impact client bundle size.
  • Improved initial page load performance.
  • Reduced memory usage on client devices.


Built-in Data Fetching


Code

1async function BlogPost({ id }) {
2  // Fetch runs on server, result included in initial HTML
3  const post = await fetch(`/api/posts/${id}`);
4  const data = await post.json();
5  return <article>{data.content}</article>;
6}
7
8
9
  • Streamlined data access patterns.
  • No useEffect for data fetching.
  • Improved SEO with complete initial HTML.


Limitations and Considerations:


Browser APIs

Code

1// This won't work in a Server Component
2function WindowSizeDisplay() {
3  // ❌ Error: window is not defined
4  const width = window.innerWidth;
5  return <div>Width: {width}px</div>;
6}
7

Interactive Features

Code

1// This needs to be a Client Component
2function Button() {
3  // ❌ Error: useState is not available in Server Components
4  const [clicked, setClicked] = useState(false);
5  return <button onClick={() => setClicked(true)}>Click me</button>;
6}
7

State Management

Code

1// This must be moved to a Client Component
2function Counter() {
3  // ❌ Error: Can't use interactive hooks in Server Components
4  const [count, setCount] = useState(0);
5  return <div>{count}</div>;
6}
7
8

Optimal Use Cases:


Data-Heavy Components


Code

1async function UserDashboard() {
2  const userData = await fetchUserData();
3  const analytics = await fetchAnalytics();
4  const recommendations = await generateRecommendations();
5  
6  return (
7    <div>
8      <UserProfile data={userData} />
9      <AnalyticsDisplay data={analytics} />
10      <Recommendations items={recommendations} />
11    </div>
12  );
13}
14
15

SEO-Critical Content

Code

1async function BlogPost() {
2  const post = await fetchPost();
3  
4  return (
5    <article>
6      <h1>{post.title}</h1>
7      <div dangerouslySetInnerHTML={{ __html: post.content }} />
8      <MetaTags 
9        title={post.title} 
10        description={post.excerpt} 
11      />
12    </article>
13  );
14}
15
16

Static Content

Code

1function PrivacyPolicy() { return (
2<div>
3  <h1>Privacy Policy</h1>
4  <LastUpdated date="2024-01-01" />
5  <PolicyContent />
6</div>
7); }
8

Server Components excel in scenarios where interactivity isn't the primary concern, but performance, SEO, and secure data access are crucial. They form the foundation of the App Router's architecture, making server-first rendering the default approach in Next.js applications.

Client Components

Client Components bring interactivity and dynamic behavior to modern Next.js apps. While they still participate in server-side rendering for the initial page load, they become fully interactive on the client side after hydration. Understanding their work is crucial for building responsive, interactive, performant applications.


Core Characteristics:


Client-Side Execution


Code

1'use client';
2
3import { useState } from 'react';
4
5export default function InteractiveForm() {
6  const [formData, setFormData] = useState({
7    name: '',
8    email: ''
9  });
10
11  const handleSubmit = async (e) => {
12    e.preventDefault();
13    await submitForm(formData);
14  };
15
16  return (
17    <form onSubmit={handleSubmit}>
18      <input
19        value={formData.name}
20        onChange={(e) => setFormData(prev => ({
21          ...prev,
22          name: e.target.value
23        }))}
24      />
25      {/* ... */}
26    </form>
27  );
28}
29
  • Full access to browser APIs
  • State management capabilities
  • Event handling support
  • Interactive UI elements


Hydration Process


Code

1'use client';
2
3import { useEffect, useState } from 'react';
4
5export default function HydratedComponent() {
6  const [isHydrated, setIsHydrated] = useState(false);
7
8  useEffect(() => {
9    setIsHydrated(true);
10  }, []);
11
12  return (
13    <div>
14      {isHydrated ? (
15        <InteractiveContent />
16      ) : (
17        <StaticFallback />
18      )}
19    </div>
20  );
21}
22
  • Initial server render for fast first paint
  • Progressive enhancement after JavaScript loads
  • Seamless transition to interactive state
  • Fallback support for non-JS environments


Browser API Integration


Code

1'use client';
2
3export default function LocationAware() {
4  const [location, setLocation] = useState(null);
5
6  useEffect(() => {
7    if ('geolocation' in navigator) {
8      navigator.geolocation.getCurrentPosition(
9        position => setLocation(position)
10      );
11    }
12  }, []);
13
14  return (
15    <div>
16      {location ? (
17        <Map coordinates={location.coords} />
18      ) : (
19        'Loading location...'
20      )}
21    </div>
22  );
23}
24
  • Access to browser-specific features
  • Integration with device APIs
  • Real-time updates and interactions
  • Client-side data persistence


Optimal Use Cases:


Interactive Forms


Code

1'use client';
2
3export default function DynamicForm() {
4  const [fields, setFields] = useState([{ id: 1, value: '' }]);
5
6  const addField = () => {
7    setFields(prev => [...prev, { 
8      id: prev.length + 1, 
9      value: '' 
10    }]);
11  };
12
13  return (
14    <form>
15      {fields.map(field => (
16        <input
17          key={field.id}
18          value={field.value}
19          onChange={e => handleChange(field.id, e)}
20        />
21      ))}
22      <button type="button" onClick={addField}>
23        Add Field
24      </button>
25    </form>
26  );
27}
28

Real-Time features or pagination implementation


Code

1'use client';
2
3export default function ChatWidget() {
4  const [messages, setMessages] = useState([]);
5
6  useEffect(() => {
7    const ws = new WebSocket('wss://chat.api');
8    
9    ws.onmessage = (event) => {
10      setMessages(prev => [...prev, event.data]);
11    };
12
13    return () => ws.close();
14  }, []);
15
16  return (
17    <div className="chat-window">
18      {messages.map(msg => (
19        <Message key={msg.id} content={msg} />
20      ))}
21    </div>
22  );
23}
24

Complex UI Interactions


Code

1'use client';
2
3export default function DragAndDropList() {
4  const [items, setItems] = useState(initialItems);
5
6  const onDragEnd = (result) => {
7    if (!result.destination) return;
8
9    const reorderedItems = Array.from(items);
10    const [removed] = reorderedItems.splice(result.source.index, 1);
11
12    reorderedItems.splice(result.destination.index, 0, removed);
13
14    setItems(reorderedItems);
15  };
16
17  return (
18    <DragDropContext onDragEnd={onDragEnd}>
19      <Droppable droppableId="list">
20        {provided => (
21          <div ref={provided.innerRef} {...provided.droppableProps}>
22            {items.map((item, index) => (
23              <DraggableItem key={item.id} item={item} index={index} />
24            ))}
25            {provided.placeholder}
26          </div>
27        )}
28      </Droppable>
29    </DragDropContext>
30  );
31}
32

Client Components are essential for building interactive features that respond to user input and maintain client-side state. They should be used judiciously, as each Client Component increases the JavaScript bundle size and adds to the hydration overhead.


The key is to keep Client Components as small and focused as possible, using them only for the interactive parts of your application while letting Server Components handle the static and data-fetching aspects.

Client Boundary

The Client Boundary is a crucial architectural concept in Next.js App Router that determines where server-side rendering ends and client-side rendering begins. Understanding this boundary is essential for optimizing application performance and managing the transition between Server and Client Components.


Understanding the Client Boundary


The Client Boundary is established by the use client directive at the top of a file. This directive tells Next.js that everything in this file needs to be treated as client-side code. 


Let's explore how this works in practice:

Code

1// app/page.tsx
2import { ClientComponent } from "@/components/client-component"
3import { ServerComponent } from "@/components/server-component"
4
5export default function Page() {
6  return (
7    <ClientComponent>
8      <ServerComponent/>
9    </ClientComponent>
10  )
11}
12

Code

1// components/client-component.tsx
2'use client';
3
4import { useState } from 'react';
5
6export function ClientComponent({ children }) {
7  const [isMenuOpen, setIsMenuOpen] = useState(false);
8
9  return (
10    <div>
11      <button onClick={() => setIsMenuOpen(!isMenuOpen)}>
12        Toggle Menu
13      </button>
14            
15      {isMenuOpen && (
16        <nav>
17          {/* Server Components cannot be used within Client Components */}
18          {/* directly, but they can be passed as 'children' or any other */}
19          {/* prop that will have JSX supplied, in this way */}
20          {/* the Client Component's only responsibility regarding this  */}
21          {/* is to decide where 'children' will be placed */}
22          {children}
23        </nav>
24      )}
25    </div>
26  );
27}
28

In this example, the ClientComponent creates a Client Boundary. Everything within this component will be part of the client bundle, even though it may contain Server Components passed as children, composed in the upper Page component.


Implications of Client Boundaries


Bundle Size Management

Code

1// ❌ Poor boundary placement
2'use client';
3import { heavyLibrary } from 'large-package';
4
5// Everything including heavyLibrary will be in client bundle
6export function App() {
7  return (
8    <div>
9      <HeavyComponent />
10      <LightComponent />
11    </div>
12  );
13}
14
15// ✅ Optimized boundary placement
16// app/page.tsx
17import { HeavyServerComponent } from './HeavyServerComponent';
18
19export function App() {
20  return (
21    <div>
22      <HeavyServerComponent />
23      <ClientInteractiveComponent />
24    </div>
25  );
26}
27
28// components/ClientInteractiveComponent.tsx
29'use client';
30export function ClientInteractiveComponent() {
31  // Only interactive code goes in client bundle
32  return <button onClick={() => alert('Clicked!')}>Click me</button>;
33}
34

Hydration Strategy

Code

1'use client';
2
3export function ComplexUI({ serverData }) {
4  const [clientData, setClientData] = useState(null);
5
6  useEffect(() => {
7    // This runs after hydration
8    fetchAdditionalData().then(setClientData);
9  }, []);
10
11  return (
12    <div>
13      {/* Server-rendered data available immediately */}
14      <ServerRenderedContent data={serverData} />
15      
16      {/* Client data loads after hydration */}
17      {clientData ? (
18        <ClientContent data={clientData} />
19      ) : (
20        <LoadingSpinner />
21      )}
22    </div>
23  );
24}
25

Component Organization

Code

1// components/feature/index.tsx
2// Entry point for a feature
3import { FeatureWrapper } from './FeatureWrapper';
4import { DataFetcher } from './DataFetcher';
5import { InteractiveUI } from './InteractiveUI';
6
7export function Feature() {
8  return (
9    <FeatureWrapper>
10      {/* Keeps data fetching on server */}
11      <DataFetcher>
12        {(data) => (
13          {/* Moves interactivity to client */}
14          <InteractiveUI initialData={data} />
15        )}
16      </DataFetcher>
17    </FeatureWrapper>
18  );
19}
20
21// components/feature/InteractiveUI.tsx
22'use client';
23export function InteractiveUI({ initialData }) {
24  // Client-side logic here
25}
26

Best Practices for Managing Client Boundaries


Minimize Boundary Size


Code

1// ❌ Large client boundary
2'use client';
3export function Page() {
4  return (
5    <div>
6      <Header />
7      <Sidebar />
8      <MainContent />
9      <Footer />
10    </div>
11  );
12}
13
14// ✅ Targeted client boundaries
15export function Page() {
16  return (
17    <div>
18      <StaticHeader />
19      <ClientSidebar />
20      <MainContent />
21      <StaticFooter />
22    </div>
23  );
24}
25
26// Only make interactive parts client components
27'use client';
28export function ClientSidebar() {
29  // Interactive sidebar logic
30}
31

Strategic Component Splitting

Code

1// components/ProductCard.tsx
2// Server component for data and structure
3export function ProductCard({ product }) {
4  return (
5    <div className="product-card">
6      <ProductImage src={product.image} />
7      <ProductInfo product={product} />
8      <AddToCartButton productId={product.id} />
9    </div>
10  );
11}
12
13// components/AddToCartButton.tsx
14// Client component for interactivity
15'use client';
16export function AddToCartButton({ productId }) {
17  return (
18    <button onClick={() => addToCart(productId)}>
19      Add to Cart
20    </button>
21  );
22}
23

Data Flow Optimization

Code

1// Optimized data flow across boundary
2export async function DataAwareComponent() {
3  // Fetch data on server
4  const data = await fetchData();
5  
6  return (
7    <div>
8      <ServerRenderedContent data={data} />
9      <ClientInteraction serverData={data} />
10    </div>
11  );
12}
13
14'use client';
15function ClientInteraction({ serverData }) {
16  // Use server-fetched data without refetching
17  const [localState, setLocalState] = useState(serverData);
18  
19  return (
20    <div>
21      <DisplayData data={localState} />
22      <UpdateButton onUpdate={setLocalState} />
23    </div>
24  );
25}
26

Understanding and properly managing Client Boundaries is crucial for building performant Next.js applications. It allows for maintaining the benefits of server-side rendering while providing rich client-side interactivity where needed.


The key is to keep client boundaries as small and focused as possible, ensuring that server-rendered content remains efficient while interactive elements are properly hydrated on the client.

Server Rendering Strategies

Static Rendering


Static rendering is like pre-cooking meals before a restaurant opens—everything is prepared and ready for serving when needed. In Next.js, static rendering prepares your pages during the build process, converting them into HTML files that can be served immediately to users. This approach offers exceptional performance since no server processing is needed at the request time.


How Static Rendering Works


Let's explore a practical example of static rendering in action:

Code

1// app/blog/page.tsx
2async function getBlogPosts() {
3  // This function runs at build time
4  const posts = await prisma.post.findMany({
5    where: { status: 'published' },
6    orderBy: { publishedAt: 'desc' }
7  });
8  
9  return posts;
10}
11
12export default async function BlogPage() {
13  // Data is fetched once at build time
14  const posts = await getBlogPosts();
15  
16  return (
17    <article>
18      <h1>Our Blog</h1>
19      {posts.map(post => (
20        <BlogPostPreview 
21          key={post.id}
22          post={post}
23        />
24      ))}
25    </article>
26  );
27}
28

In this example, the blog page and all its content are generated during the build process. When users request the page, they receive the pre-rendered HTML immediately, resulting in speedy page loads. It’s worth noting, that since the code doesn’t opt into dynamic behavior (like accessing cookies()) or force dynamic rendering (by exporting const dynamic = "force-dynamic"), Next.js will just default to static rendering automatically.


Static Rendering with Dynamic Routes


Often, you'll need to generate multiple static pages based on your data. Here's how to handle that:

Code

1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3  // This runs at build time to generate all possible routes
4  const posts = await prisma.post.findMany({
5    select: { slug: true }
6  });
7  
8  return posts.map(post => ({
9    slug: post.slug
10  }));
11}
12
13export default async function BlogPost({ params }) {
14  const post = await prisma.post.findUnique({
15    where: { slug: params.slug }
16  });
17  
18  return (
19    <article>
20      <h1>{post.title}</h1>
21      <div dangerouslySetInnerHTML={{ __html: post.content }} />
22    </article>
23  );
24}
25

Optimizing Static Generation


Consider a documentation site that needs to generate hundreds of pages. We can optimize the build process:

Code

1// app/docs/[...slug]/page.tsx
2export async function generateStaticParams() {
3  // Implement parallel processing for faster builds
4  const allDocs = await fetchAllDocuments();
5  
6  // Process in batches to manage memory
7  const batchSize = 50;
8  const batches = chunk(allDocs, batchSize);
9  
10  const params = [];
11  for (const batch of batches) {
12    const batchParams = await Promise.all(
13      batch.map(async doc => ({
14        slug: doc.path.split('/'),
15        lastMod: doc.lastModified
16      }))
17    );
18    params.push(...batchParams);
19  }
20  
21  return params;
22}
23

Static Data Requirements


Sometimes you need to fetch data that's used across multiple static pages:

Code

1// app/layout.tsx
2export async function generateMetadata() {
3  // This data is fetched once and reused across all static pages
4  const siteConfig = await fetchSiteConfig();
5  
6  return {
7    title: {
8      template: `%s | ${siteConfig.siteName}`,
9      default: siteConfig.siteName
10    },
11    description: siteConfig.description
12  };
13}
14

Incremental Static Regeneration (ISR)


Incremental Static Regeneration allows for updating static pages incrementally by regenerating them in the background after a specified time interval. This approach combines the benefits of static site generation with dynamic content updates. When requested, Next.js serves the cached page if available and triggers regeneration if the cache is stale, ensuring users get fast load times while keeping content fresh.


Let’s go back to our previous example, and see how we can implement ISR here:


Code

1// app/blog/page.tsx
2
3// exporting const named `revalidate` enables ISR for specified time interval
4export const revalidate = 60 // seconds
5
6async function getBlogPosts() {
7// This function runs at build time to generate the static page, and when a request arrives after the time specified in revalidate has elapsed since the last generation, it re-runs in the background to regenerate the page.
8  const posts = await prisma.post.findMany({
9    where: { status: 'published' },
10    orderBy: { publishedAt: 'desc' }
11  });
12  
13  return posts;
14}
15
16export default async function BlogPage() {
17  const posts = await getBlogPosts();
18  
19  return (
20    <article>
21      <h1>Our Blog</h1>
22      {posts.map(post => (
23        <BlogPostPreview 
24          key={post.id}
25          post={post}
26        />
27      ))}
28    </article>
29  );
30}
31

When to Use Static Rendering


Static rendering is ideal for content that:


  1. Is the same for all users.
  2. Can be determined at build time.
  3. Doesn't need frequent updates.


Common use cases include:


Code

1// Marketing pages
2// app/about/page.tsx
3export default function AboutPage() {
4  return (
5    <div>
6      <h1>About Our Company</h1>
7      <CompanyHistory />
8      <TeamSection />
9      <ContactInfo />
10    </div>
11  );
12}
13
14// Documentation
15// app/docs/[...path]/page.tsx
16export default async function DocPage({ params }) {
17  const doc = await getDocContent(params.path);
18  
19  return (
20    <div className="doc-page">
21      <TableOfContents headings={doc.headings} />
22      <MarkdownRenderer content={doc.content} />
23      <PreviousNextLinks />
24    </div>
25  );
26}
27
28// Product listings with infrequent updates
29// app/products/page.tsx
30export default async function ProductsPage() {
31  const products = await getProducts();
32  
33  return (
34    <div className="products-grid">
35      {products.map(product => (
36        <ProductCard 
37          key={product.id}
38          product={product}
39          staticImage // Images are optimized at build time
40        />
41      ))}
42    </div>
43  );
44}
45

Limitations and Considerations


While static rendering offers superior performance, it comes with some trade-offs:


Build Time Impact

Code

1// Consider build time for large datasets
2export async function generateStaticParams() {
3  if (process.env.SKIP_LONG_BUILDS) {
4    // During development, generate fewer pages
5    return getLimitedStaticPaths();
6  }
7  
8  // In production, generate all pages
9  return getAllStaticPaths();
10}
11

Content Freshness

Code

1// app/news/page.tsx
2// This might not be suitable for static rendering
3export default async function NewsPage() {
4  // Content will be stale until next build unless ISR is used
5  const news = await getLatestNews();
6  
7  return (
8    <NewsFeed items={news} />
9  );
10}
11

Memory Usage During Build


When statically generating thousands of pages (e.g., blog posts), memory usage and build time can grow significantly.

Code

1export async function generateStaticParams() {
2  const totalPages = await getAllPosts(); // fetching thousand of entries 
3  
4  return posts.map((post) => ({
5    slug: post.slug,
6  }));
7

Best Practices for Static Rendering


Selective Static Generation


Code

1// Mix static and dynamic content when needed
2export default function HomePage() {
3  return (
4    <div>
5      <StaticHeader />
6      <StaticFeatures />
7      {/* Dynamic content can be loaded client-side */}
8      <Suspense fallback={<Loading />}>
9        <DynamicNewsFeed />
10      </Suspense>
11    </div>
12  );
13}
14

Error Handling

Code

1export default async function Page({ params }) {
2  try {
3    const data = await fetchStaticData(params);
4    return <PageContent data={data} />;
5  } catch (error) {
6    // Generate a static error page
7    return <ErrorPage statusCode={500} />;
8  }
9}
10

Static rendering is a powerful feature that can significantly improve your application's performance when used appropriately. The key lies in understanding when to use it and optimizing the build process for your specific needs.

Dynamic Rendering

Dynamic rendering generates page content on the server in response to each request. This solution ensures users always receive up-to-date information, but requires server processing for every page load. In this case, content is generated on-demand for each request, serving the most up-to-date information. Unlike static rendering, which serves pre-built pages, dynamic rendering processes each request individually on the server.


Understanding Dynamic Rendering


When a page is rendered dynamically, Next.js executes all the code on the server for each request. It means you can access request-specific information and generate personalized content. Here's a practical example:



Code

1// app/dashboard/page.tsx
2export default async function DashboardPage() {
3  // This function runs for every request
4  const user = await getCurrentUser();
5  const userPreferences = await getUserPreferences(user.id);
6  const recommendations = await getPersonalizedContent(user.id);
7  
8  return (
9    <div className="dashboard">
10      <WelcomeHeader 
11        name={user.name}
12        lastLogin={user.lastLogin}
13      />
14      <DashboardContent 
15        preferences={userPreferences}
16        recommendations={recommendations}
17      />
18    </div>
19  );
20}
21

In this example, each user sees their personalized dashboard. The server generates the content specifically for them when they request the page.


Request-Time Data Access


Dynamic rendering allows you to work with request-specific data, including cookies, headers, and URL parameters:

Code

1// app/api/user-region/page.tsx
2import { headers, cookies } from 'next/headers';
3
4export default async function UserRegionPage() {
5  // Access request headers
6  const headersList = headers();
7  const userCountry = headersList.get('x-country-code');
8  
9  // Access cookies
10  const cookieStore = cookies();
11  const userPreferences = cookieStore.get('preferences');
12  
13  // Fetch region-specific content
14  const content = await getRegionalContent(userCountry);
15  
16  return (
17    <div>
18      <RegionalHeader country={userCountry} />
19      <ContentDisplay 
20        content={content}
21        preferences={userPreferences}
22      />
23    </div>
24  );
25}
26

Real-Time Data Requirements


Some applications need to display real-time information that changes frequently:

Code

1// app/stocks/[symbol]/page.tsx
2export default async function StockPage({ params }) {
3  // Fetch real-time stock data for each request
4  const stockData = await getStockPrice(params.symbol);
5  const companyInfo = await getCompanyInfo(params.symbol);
6  const marketTrends = await getMarketTrends();
7  
8  return (
9    <div className="stock-page">
10      <StockPriceDisplay 
11        currentPrice={stockData.price}
12        change={stockData.change}
13      />
14      <CompanyOverview info={companyInfo} />
15      <MarketContext trends={marketTrends} />
16    </div>
17  );
18}
19

Handling User Authentication


Dynamic rendering is particularly useful for authenticated content:

Code

1// app/account/settings/page.tsx
2import { redirect } from 'next/navigation';
3
4export default async function AccountSettings() {
5  const session = await getSession();
6  
7  // Redirect if not authenticated
8  if (!session) {
9    redirect('/login');
10  }
11  
12  // Fetch user-specific data
13  const settings = await getUserSettings(session.user.id);
14  const securityInfo = await getSecurityDetails(session.user.id);
15  
16  return (
17    <div className="settings-page">
18      <UserProfileSection 
19        user={session.user}
20        settings={settings}
21      />
22      <SecuritySettings 
23        info={securityInfo}
24        enabled={settings.securityFeatures}
25      />
26    </div>
27  );
28}
29

Optimizing Dynamic Content


While dynamic rendering ensures fresh content, it's important to optimize performance:


Caching Strategies (request deduping)


Code

1// utils/cache.ts
2import { cache } from 'react';
3
4export const getUser = cache(async (userId: string) => {
5  // This will be cached for the duration of the request
6  const user = await db.user.findUnique({
7    where: { id: userId }
8  });
9  return user;
10});
11
12// app/profile/[id]/page.tsx
13export default async function ProfilePage({ params }) {
14  // Multiple components can call getUser without duplicate fetches
15  const user = await getUser(params.id);
16  
17  return (
18    <>
19      <ProfileHeader user={user} />
20      <ProfileContent user={user} />
21      <ProfileSidebar user={user} />
22    </>
23  );
24}
25

Partial Static Content

Code

1// app/product/[id]/page.tsx
2export default async function ProductPage({ params }) {
3  return (
4    <div className="product-layout">
5      {/* Static content */}
6      <StaticSidebar />
7      <StaticFooter />
8      
9      {/* Dynamic content */}
10      <Suspense fallback={<Loading />}>
11        <DynamicProductInfo id={params.id} />
12      </Suspense>
13    </div>
14  );
15}
16

Common Use Cases


Dynamic rendering is ideal for:


Personalized Content


Code

1// app/feed/page.tsx
2export default async function PersonalizedFeed() {
3  const user = await getCurrentUser();
4  const feed = await generatePersonalizedFeed(user.id);
5  
6  return (
7    <div className="feed">
8      {feed.map(item => (
9        <FeedItem 
10          key={item.id}
11          item={item}
12          userPreferences={user.preferences}
13        />
14      ))}
15    </div>
16  );
17}
18

Part of e-commerce products details page that is above the fold

Code

1// app/products/[id]/page.tsx
2export default async function ProductPage({ params }) {
3  const product = await getProduct(params.id);
4  const inventory = await getRealtimeInventory(params.id);
5  const pricing = await getCurrentPricing(params.id);
6  
7  return (
8    <div className="product-page">
9      <ProductInfo product={product} />
10      <InventoryStatus stock={inventory.available} />
11      <PricingDisplay 
12        price={pricing.current}
13        discounts={pricing.activeDiscounts}
14      />
15    </div>
16  );
17}
18

Analytics Dashboards

Code

1// app/analytics/page.tsx
2export default async function AnalyticsDashboard() {
3  const startDate = new Date();
4  startDate.setDate(startDate.getDate() - 30);
5  
6  const metrics = await getAnalyticsData(startDate);
7  const trends = await analyzeTrends(metrics);
8  
9  return (
10    <div className="analytics-dashboard">
11      <MetricsOverview data={metrics} />
12      <TrendAnalysis trends={trends} />
13      <RealtimeVisitors />
14    </div>
15  );
16}
17

Understanding Trade-offs


Dynamic rendering comes with certain considerations:


Server Load

Code

1// Implement rate limiting for expensive operations
2import { rateLimit } from './utils/rate-limit';
3
4export async function GET(request: Request) {
5  const identifier = request.headers.get('x-forwarded-for') || 'anonymous';
6  const limit = await rateLimit(identifier);
7  
8  if (!limit.success) {
9    return new Response('Too Many Requests', {
10      status: 429,
11      headers: {
12        'Retry-After': limit.reset
13      }
14    });
15  }
16  
17  // Process request normally
18}
19

Response Time

Code

1// Implement timeout handling
2async function fetchWithTimeout(resource: string, timeout: number) {
3  const controller = new AbortController();
4  const id = setTimeout(() => controller.abort(), timeout);
5  
6  try {
7    const response = await fetch(resource, {
8      signal: controller.signal
9    });
10    return response;
11  } catch (error) {
12    if (error.name === 'AbortError') {
13      throw new Error('Request timed out');
14    }
15    throw error;
16  } finally {
17    clearTimeout(id);
18  }
19}
20

Cache Strategy

Code

1// Implement custom caching for specific routes
2export async function GET(request: Request) {
3  const cacheKey = new URL(request.url).pathname;
4  const cachedResponse = await cache.get(cacheKey);
5  
6  if (cachedResponse) {
7    return new Response(cachedResponse, {
8      headers: {
9        'Cache-Control': 'public, max-age=60',
10        'X-Cache': 'HIT'
11      }
12    });
13  }
14  
15  const data = await generateResponse();
16  await cache.set(cacheKey, data, 60); // Cache for 60 seconds
17  
18  return new Response(data, {
19    headers: {
20      'Cache-Control': 'public, max-age=60',
21      'X-Cache': 'MISS'
22    }
23  });
24}
25

Dynamic rendering provides the flexibility needed for personalized, real-time content while requiring careful consideration of performance implications. The key is to use it carefully, implementing appropriate caching strategies and optimization techniques to ensure a responsive user experience.

Streaming

Streaming is a technique that enables progressive page rendering, where content is sent to the client in chunks as it becomes available rather than waiting for the entire page to be ready. Rather than waiting for all content to be generated before sending anything to the user, the application can start sending pieces of the UI as soon as they're ready.


Understanding Streaming


In traditional server rendering, the server must complete all data fetching and rendering before sending any content to the client. If data requests are slow, It can lead to a slower Time to First Byte (TTFB). Streaming solves this issue by breaking the response into smaller chunks and sending them progressively. Let's see how this works in practice:


Code

1// app/profile/page.tsx
2import { Suspense } from 'react';
3
4export default async function ProfilePage() {
5  // The header will be sent immediately
6  return (
7    <div className="profile-page">
8      <Header />
9      
10      {/* User info will stream in when ready */}
11      <Suspense fallback={<UserInfoSkeleton />}>
12        <UserInfo />
13      </Suspense>
14      
15      {/* Activity feed will stream in independently */}
16      <Suspense fallback={<ActivityFeedSkeleton />}>
17        <ActivityFeed />
18      </Suspense>
19      
20      {/* Recommendations can load last */}
21      <Suspense fallback={<RecommendationsSkeleton />}>
22        <Recommendations />
23      </Suspense>
24    </div>
25  );
26}
27
28// Components with their own data fetching
29async function UserInfo() {
30  const user = await fetchUserData(); // Takes 500ms
31  return <UserProfile user={user} />;
32}
33
34async function ActivityFeed() {
35  const activities = await fetchActivities(); // Takes 1000ms
36  return <Feed items={activities} />;
37}
38
39async function Recommendations() {
40  const recommendations = await fetchRecommendations(); // Takes 2000ms
41  return <RecommendedItems items={recommendations} />;
42

In this example, instead of waiting for all data fetching to complete (which would take 3.5 seconds total), the user sees the header immediately and:


  1. User info after 500ms.
  2. Activity feed after 1000ms.
  3. Recommendations after 2000ms.


This progressive loading creates a more engaging user experience, as content appears incrementally rather than all at once after a long wait.


Loading UI and Suspense Boundaries


The loading UI is crucial for maintaining user engagement during streaming. Let's explore how to create effective loading states:


Code

1// components/LoadingStates.tsx
2export function UserInfoSkeleton() {
3  return (
4    <div className="animate-pulse">
5      <div className="h-20 w-20 rounded-full bg-gray-200" />
6      <div className="mt-4 h-4 w-48 bg-gray-200" />
7      <div className="mt-2 h-4 w-32 bg-gray-200" />
8    </div>
9  );
10}
11
12// app/complex-page/page.tsx
13export default function ComplexPage() {
14  return (
15    <div className="grid grid-cols-12 gap-4">
16      <Suspense 
17        fallback={<UserInfoSkeleton />}
18        key="user-info"
19      >
20        <UserSection />
21      </Suspense>
22      
23      {/* Nested suspense boundaries */}
24      <section className="col-span-8">
25        <Suspense fallback={<ContentSkeleton />}>
26          <AsyncContent>
27            <Suspense fallback={<DetailsSkeleton />}>
28              <AsyncDetails />
29            </Suspense>
30          </AsyncContent>
31        </Suspense>
32      </section>
33    </div>
34  );
35}
36

Streaming with Data Requirements


Sometimes components need data from multiple sources. Here's how to handle complex data requirements while streaming:

Code

1// app/dashboard/page.tsx
2export default async function Dashboard() {
3  // This data is needed for all child components
4  const user = await getCurrentUser();
5  
6  return (
7    <div className="dashboard">
8      <Header user={user} />
9      
10      <div className="dashboard-grid">
11        {/* Each section streams independently */}
12        <Suspense fallback={<StatsSkeleton />}>
13          <UserStats userId={user.id} />
14        </Suspense>
15        
16        <Suspense fallback={<ChartsSkeleton />}>
17          <AsyncCharts userId={user.id} />
18        </Suspense>
19        
20        <Suspense 
21          fallback={<TableSkeleton />}
22          key={user.id} // Reset suspense when user changes
23        >
24          <DataTable userId={user.id} />
25        </Suspense>
26      </div>
27    </div>
28  );
29}
30
31// Components can have their own loading states
32async function UserStats({ userId }) {
33  const stats = await fetchUserStats(userId);
34  
35  return (
36    <div className="stats-grid">
37      {stats.map(stat => (
38        <StatCard
39          key={stat.id}
40          value={stat.value}
41          label={stat.label}
42        />
43      ))}
44    </div>
45  );
46}
47

Optimizing Streaming Performance


To make the most of streaming, consider these optimization techniques:


Prioritize Critical Content

Code

1export default function ProductPage() {
2  return (
3    <>
4      {/* Critical content outside Suspense */}
5      <ProductHeader />
6      <MainProductImage />
7      
8      {/* Less critical content streams in */}
9      <Suspense fallback={<RelatedSkeleton />}>
10        <RelatedProducts />
11      </Suspense>
12      
13      <Suspense fallback={<ReviewsSkeleton />}>
14        <ProductReviews />
15      </Suspense>
16    </>
17  );
18}
19

Parallel Data Fetching

Code

1async function ParallelDataSection() {
2  // Start both fetches immediately
3  const productPromise = fetchProducts();
4  const categoryPromise = fetchCategories();
5  
6  // Wait for both in parallel
7  const [products, categories] = await Promise.all([
8    productPromise,
9    categoryPromise
10  ]);
11  
12  return (
13    <section>
14      <ProductGrid products={products} />
15      <CategoryList categories={categories} />
16    </section>
17  );
18}
19

Waterfall Prevention

Code

1// ❌ Avoid data fetching waterfalls
2async function SlowComponent() {
3  const data1 = await fetch1(); // Wait
4  const data2 = await fetch2(); // Then wait again
5  const data3 = await fetch3(); // And again
6  
7  return <Display data1={data1} data2={data2} data3={data3} />;
8}
9
10// ✅ Fetch in parallel
11async function FastComponent() {
12  // Start all fetches immediately
13  const [data1, data2, data3] = await Promise.all([
14    fetch1(),
15    fetch2(),
16    fetch3()
17  ]);
18  
19  return <Display data1={data1} data2={data2} data3={data3} />;
20}
21

Real-World Streaming Patterns


Let's look at some common patterns for implementing streaming in different scenarios:


Social Media Feed

Code

1export default function FeedPage() {
2  return (
3    <div className="feed-layout">
4      {/* Main feed streams in chunks */}
5      <Suspense fallback={<FeedSkeleton />}>
6        <InfiniteFeed />
7      </Suspense>
8      
9      {/* Sidebar content streams separately */}
10      <aside>
11        <Suspense fallback={<TrendingSkeleton />}>
12          <TrendingTopics />
13        </Suspense>
14        
15        <Suspense fallback={<SuggestionsSkeleton />}>
16          <SuggestedUsers />
17        </Suspense>
18      </aside>
19    </div>
20  );
21}
22

E-commerce Category Page

Code

1export default function CategoryPage({ params }) {
2  return (
3    <div className="category-page">
4      {/* Critical filters load first */}
5      <Suspense fallback={<FiltersSkeleton />}>
6        <ProductFilters category={params.category} />
7      </Suspense>
8      
9      <div className="product-grid">
10        {/* Products stream in batches */}
11        <Suspense fallback={<ProductGridSkeleton />}>
12          <StreamingProductGrid category={params.category} />
13        </Suspense>
14      </div>
15      
16      {/* Load faceted search last */}
17      <Suspense fallback={<FacetsSkeleton />}>
18        <SearchFacets category={params.category} />
19      </Suspense>
20    </div>
21  );
22}
23

Analytics Dashboard

Code

1export default function AnalyticsDashboard() {
2  return (
3    <div className="dashboard-grid">
4      {/* Quick metrics load first */}
5      <Suspense fallback={<QuickStatsSkeleton />}>
6        <QuickStats />
7      </Suspense>
8      
9      {/* Charts stream in progressively */}
10      <div className="charts-grid">
11        <Suspense fallback={<ChartSkeleton />}>
12          <RevenueChart />
13        </Suspense>
14        
15        <Suspense fallback={<ChartSkeleton />}>
16          <UserRetentionChart />
17        </Suspense>
18        
19        <Suspense fallback={<ChartSkeleton />}>
20          <ConversionChart />
21        </Suspense>
22      </div>
23      
24      {/* Detailed tables load last */}
25      <Suspense fallback={<DetailsSkeleton />}>
26        <DetailedMetrics />
27      </Suspense>
28    </div>
29  );
30}
31

Streaming is a powerful feature that can significantly improve your application's perceived performance. The key here is to identify which parts of your UI can be loaded progressively and implement appropriate loading states that maintain visual stability as content streams in.


By combining streaming with parallel data fetching and proper prioritization, you can create highly responsive applications. This approach ensures a great user experience, even when working with slow data sources or complex UI requirements.


Partial Pre-rendering (PPR)


Partial Pre-rendering (PPR) is a hybrid rendering approach that combines static and dynamic rendering at the component level. It pre-renders a static shell of your page and streams in dynamic content as needed. This way, you get the speed of static rendering and the freshness of dynamic content at the same time.


Understanding PPR


Partial Pre-rendering represents a fundamental shift in how we render web pages. Instead of choosing between static and dynamic rendering for an entire page, PPR allows us to make this choice at the component level. Here's how it works:


Code

1// app/products/page.tsx
2export default async function ProductPage() {
3  // This part becomes the static shell
4  return (
5    <div className="product-page">
6      <Header />  {/* Static */}
7      <Navigation />  {/* Static */}
8      
9      {/* Dynamic hole in the static shell */}
10      <Suspense fallback={<ProductGridSkeleton />}>
11        <ProductGrid />  {/* Rendered at request time */}
12      </Suspense>
13    </div>
14  );
15}
16
17// The static parts are pre-rendered and cached
18function Header() {
19  return (
20    <header>
21      <h1>Our Products</h1>
22      <nav>
23        <Link href="/categories">Categories</Link>
24        <Link href="/deals">Deals</Link>
25      </nav>
26    </header>
27  );
28}
29
30// Dynamic content streams in after the static shell loads
31async function ProductGrid() {
32  // This code runs at request time
33  const products = await fetchLatestProducts();
34  
35  return (
36    <div className="grid">
37      {products.map(product => (
38        <ProductCard 
39          key={product.id}
40          product={product}
41        />
42      ))}
43    </div>
44  );
45}
46

The PPR Process


Let's break down how PPR processes a page request:


Initial Response

Code

1// When the page is first requested:
2export default function StorePage() {
3  return (
4    <>
5      {/* Immediate static response */}
6      <StoreLayout>  {/* Pre-rendered */}
7        <CategoryNav />  {/* Pre-rendered */}
8        
9        {/* Placeholder for dynamic content */}
10        <Suspense fallback={<ProductsSkeleton />}>
11          <Products />  {/* Streams in */}
12        </Suspense>
13      </StoreLayout>
14    </>
15  );
16}
17

Dynamic Content Integration

Code

1// The dynamic part that streams in:
2async function Products() {
3  // Real-time data fetching
4  const products = await getProducts();
5  const inventory = await getInventory();
6  
7  return (
8    <div className="products-grid">
9      {products.map(product => (
10        <ProductCard
11          key={product.id}
12          product={product}
13          stock={inventory[product.id]}
14        />
15      ))}
16    </div>
17  );
18}
19

Implementing PPR Effectively


Let's look at some patterns for implementing PPR in different scenarios:


E-commerce Product Page

Code

1export default function ProductPage({ params }) {
2  return (
3    <div className="product-layout">
4      {/* Static product framework */}
5      <ProductBreadcrumb category={params.category} />
6      <ProductImageGallery id={params.id} />
7      
8      {/* Dynamic, real-time elements */}
9      <Suspense fallback={<PricingSkeleton />}>
10        <DynamicPricing id={params.id} />
11      </Suspense>
12      
13      <Suspense fallback={<InventorySkeleton />}>
14        <InventoryStatus id={params.id} />
15      </Suspense>
16      
17      {/* Static content again */}
18      <ProductDescription id={params.id} />
19      
20      {/* More dynamic content */}
21      <Suspense fallback={<ReviewsSkeleton />}>
22        <CustomerReviews id={params.id} />
23      </Suspense>
24    </div>
25  );
26}
27

News Article with Live Comments

Code

1export default function ArticlePage({ params }) {
2  return (
3    <article className="news-article">
4      {/* Pre-rendered article content */}
5      <ArticleHeader id={params.id} />
6      <ArticleBody id={params.id} />
7      
8      {/* Dynamic social engagement */}
9      <Suspense fallback={<SocialStatsSkeleton />}>
10        <LiveSocialStats id={params.id} />
11      </Suspense>
12      
13      {/* Live comments section */}
14      <Suspense fallback={<CommentsSkeleton />}>
15        <LiveComments id={params.id} />
16      </Suspense>
17    </article>
18  );
19}
20

Optimizing PPR Performance


To get the most out of PPR, consider these optimization strategies:


Static Shell Optimization

Code

1export default function DashboardPage() {
2  return (
3    <div className="dashboard">
4      {/* Optimize the static shell for largest contentful paint */}
5      <DashboardHeader />  {/* Static, critical for LCP */}
6      <SideNavigation />  {/* Static navigation */}
7      
8      {/* Progressive enhancement with dynamic content */}
9      <div className="dashboard-content">
10        <Suspense fallback={<WidgetsSkeleton />}>
11          <DynamicWidgets />
12        </Suspense>
13      </div>
14    </div>
15  );
16}
17

Loading State Design

Code

1function WidgetsSkeleton() {
2  // Match exact dimensions of dynamic content
3  return (
4    <div className="widgets-grid" style={{ height: '500px' }}>
5      {Array.from({ length: 4 }).map((_, i) => (
6        <div 
7          key={i}
8          className="animate-pulse bg-gray-200 rounded-lg"
9          style={{ height: '200px' }}
10        />
11      ))}
12    </div>
13  );
14}
15

Parallel Data Fetching for Dynamic Parts

Code

1async function DynamicWidgets() {
2  // Start all data fetches immediately
3  const [
4    analyticsPromise,
5    inventoryPromise,
6    ordersPromise
7  ] = await Promise.all([
8    fetchAnalytics(),
9    fetchInventory(),
10    fetchOrders()
11  ]);
12
13  return (
14    <div className="widgets-grid">
15      <AnalyticsWidget data={analyticsPromise} />
16      <InventoryWidget data={inventoryPromise} />
17      <OrdersWidget data={ordersPromise} />
18    </div>
19  );
20}
21

Real-World PPR Patterns


Here are some common patterns for implementing PPR in different scenarios:


Social Media Profile

Code

1export default function ProfilePage({ params }) {
2  return (
3    <div className="profile-page">
4      {/* Static profile shell */}
5      <ProfileHeader userId={params.id} />
6      <Bio userId={params.id} />
7      
8      {/* Dynamic content areas */}
9      <div className="profile-content">
10        <Suspense fallback={<StoriesSkeleton />}>
11          <UserStories userId={params.id} />
12        </Suspense>
13        
14        <Suspense fallback={<PostsSkeleton />}>
15          <UserPosts userId={params.id} />
16        </Suspense>
17        
18        <Suspense fallback={<ActivitySkeleton />}>
19          <RecentActivity userId={params.id} />
20        </Suspense>
21      </div>
22    </div>
23  );
24}
25

Real-time Dashboard

Code

1export default function AnalyticsDashboard() {
2  return (
3    <div className="dashboard">
4      {/* Static dashboard framework */}
5      <DashboardHeader />
6      <NavigationTabs />
7      
8      {/* Real-time metrics */}
9      <div className="metrics-grid">
10        <Suspense fallback={<MetricsSkeleton />}>
11          <LiveMetrics />
12        </Suspense>
13      </div>
14      
15      {/* Streaming charts */}
16      <div className="charts-section">
17        <Suspense fallback={<ChartsSkeleton />}>
18          <LiveCharts />
19        </Suspense>
20      </div>
21      
22      {/* Real-time alerts */}
23      <Suspense fallback={<AlertsSkeleton />}>
24        <LiveAlerts />
25      </Suspense>
26    </div>
27  );
28}
29

PPR significantly advances web rendering strategies, offering the best static and dynamic rendering. By planning what to pre-render and what to load dynamically, you can build fast applications with instant page loads and fresh content.


The key to successful PPR implementation lies in understanding your application's needs and designing your component structure to make the most out of this hybrid approach.

Client Rendering Strategies

Client-side rendering in App Router:


  • is limited to Client Boundaries,
  • is useful for highly interactive sections,
  • impacts initial load performance,
  • and serves best for post-hydration interactions.

Pages Router Rendering Strategies

The Pages Router in Next.js provides three main rendering strategies:

  • Server-Side Rendering (SSR),
  • Static Site Generation (SSG),
  • and Client-Side Rendering (CSR).


The App Router offers more granular control with Server and Client Components. Still, understanding traditional rendering approaches is valuable, especially for maintaining existing apps or using simpler patterns when they’re enough.


Dynamic (Server-Side Rendering)


Server-Side Rendering in the Pages Router generates HTML for each request, making it ideal for pages that need fresh data or user-specific content. Let's explore how to implement SSR effectively:

Code

1// pages/posts/[id].tsx
2import { GetServerSideProps } from 'next';
3
4interface Post {
5  id: string;
6  title: string;
7  content: string;
8  author: {
9    name: string;
10    avatar: string;
11  };
12}
13
14// This runs on every request
15export const getServerSideProps: GetServerSideProps = async (context) => {
16  const { id } = context.params;
17  const { req, res } = context;
18  
19  // Access request-specific data
20  const userAgent = req.headers['user-agent'];
21  const cookies = req.cookies;
22  
23  try {
24    // Fetch data for this specific request
25    const post = await fetchPost(id);
26    const author = await fetchAuthor(post.authorId);
27    
28    // Set cache headers for performance
29    res.setHeader(
30      'Cache-Control',
31      'public, s-maxage=10, stale-while-revalidate=59'
32    );
33    
34    return {
35      props: {
36        post,
37        author,
38        userAgent,
39      },
40    };
41  } catch (error) {
42    // Handle errors gracefully
43    return {
44      notFound: true, // Returns 404 page
45    };
46  }
47};
48
49function PostPage({ post, author, userAgent }: {
50  post: Post;
51  author: Author;
52  userAgent: string;
53}) {
54  return (
55    <article className="post">
56      <header>
57        <h1>{post.title}</h1>
58        <AuthorInfo author={author} />
59      </header>
60      
61      <div className="content">
62        {post.content}
63      </div>
64      
65      {/* Conditional rendering based on device */}
66      {userAgent.includes('Mobile') ? (
67        <MobileShareButtons />
68      ) : (
69        <DesktopShareButtons />
70      )}
71    </article>
72  );
73}
74
75export default PostPage;
76

Performance Optimization for SSR


Server-Side Rendering can be optimized in several ways:


Request-Level Caching (usually implemented along with serverless redis or some serverless key-value storages)

Code

1// utils/cache.ts
2const cache = new Map();
3
4export async function cachifiedFetch(key: string, fetchFn: () => Promise<any>) {
5  if (cache.has(key)) {
6    return cache.get(key);
7  }
8  
9  const data = await fetchFn();
10  cache.set(key, data);
11  
12  // Clear cache after 1 minute
13  setTimeout(() => {
14    cache.delete(key);
15  }, 60000);
16  
17  return data;
18}
19
20// Usage in getServerSideProps
21export const getServerSideProps: GetServerSideProps = async ({ params }) => {
22  const data = await cachifiedFetch(
23    `post-${params.id}`,
24    () => fetchPost(params.id)
25  );
26  
27  return { props: { data } };
28};
29

Parallel Data Fetching

Code

1export const getServerSideProps: GetServerSideProps = async ({ params }) => {
2  // Fetch multiple data sources in parallel
3  const [post, comments, relatedPosts] = await Promise.all([
4    fetchPost(params.id),
5    fetchComments(params.id),
6    fetchRelatedPosts(params.id)
7  ]);
8  
9  return {
10    props: {
11      post,
12      comments,
13      relatedPosts
14    }
15  };
16};
17
18

Static Site Generation (SSG)

Static Site Generation pre-renders pages at build time, providing optimal performance for content that doesn't need to be real-time. Let's explore different SSG patterns:


Basic Static Page

Code

1// pages/about.tsx
2export default function AboutPage() {
3  return (
4    <div className="about">
5      <h1>About Our Company</h1>
6      <p>Static content that rarely changes...</p>
7    </div>
8  );
9}
10

Data-Dependent Static Page

Code

1// pages/blog/index.tsx
2export async function getStaticProps() {
3  // This runs at build time in production
4  const posts = await fetchBlogPosts();
5  
6  return {
7    props: {
8      posts,
9      generatedAt: new Date().toISOString(),
10    },
11    // Optional: Revalidate every hour
12    revalidate: 3600
13  };
14}
15
16function BlogIndex({ posts, generatedAt }) {
17  return (
18    <div className="blog">
19      <small>Last updated: {generatedAt}</small>
20      {posts.map(post => (
21        <BlogPreview key={post.id} post={post} />
22      ))}
23    </div>
24  );
25}
26

Dynamic Routes with SSG

Code

1// pages/products/[category]/[id].tsx
2export async function getStaticPaths() {
3  // Generate paths for most popular products
4  const popularProducts = await fetchPopularProducts();
5  
6  return {
7    paths: popularProducts.map(product => ({
8      params: {
9        category: product.category,
10        id: product.id.toString()
11      }
12    })),
13    // Generate other pages on demand
14    fallback: true
15  };
16}
17
18export async function getStaticProps({ params }) {
19  try {
20    const product = await fetchProduct(params.id);
21    
22    return {
23      props: { product },
24      revalidate: 3600 // Revalidate every hour
25    };
26  } catch (error) {
27    return { notFound: true };
28  }
29}
30

Incremental Static Regeneration (ISR)

ISR extends SSG by allowing static pages to be updated after they're built, providing a balance between performance and freshness:

Code

1// pages/products/[id].tsx
2export async function getStaticProps({ params }) {
3  const product = await fetchProduct(params.id);
4  
5  return {
6    props: {
7      product,
8      lastFetched: Date.now()
9    },
10    // Revalidate this page every minute
11    revalidate: 60
12  };
13}
14
15export async function getStaticPaths() {
16  return {
17    // Pre-render nothing at build time
18    paths: [],
19    // Generate all pages on-demand
20    fallback: 'blocking'
21  };
22}
23
24

Advanced ISR Patterns


On-Demand Revalidation


Code

1// pages/api/revalidate.ts
2export default async function handler(req, res) {
3  /* 
4  Check for secret token. NOTE: This can be prone to time-based attacks,
5  for extremely secure setup it is advised to use timingSafeEqual from 
6  `node:crypto` or a polyfill 
7  */
8  if (req.query.token !== process.env.REVALIDATION_TOKEN) {
9    return res.status(401).json({ message: 'Invalid token' });
10  }
11  
12  try {
13    // Revalidate the specific page
14    await res.revalidate('/path/to/page');
15    return res.json({ revalidated: true });
16  } catch (err) {
17    return res.status(500).send('Error revalidating');
18  }
19}
20

Conditional Revalidation

Code

1export async function getStaticProps({ params }) {
2  const product = await fetchProduct(params.id);
3  
4  // Different revalidation times based on product type
5  const revalidate = product.type === 'perishable' ? 300 : 3600;
6  
7  return {
8    props: { product },
9    revalidate
10  };
11}
12

Client-Side Rendering (CSR)

While Next.js emphasizes server-side and static rendering, there are cases where client-side rendering is appropriate:

Code

1// pages/dashboard.tsx
2import { useEffect, useState } from 'react';
3
4export default function Dashboard() {
5  const [data, setData] = useState(null);
6  const [loading, setLoading] = useState(true);
7  
8  useEffect(() => {
9    async function loadDashboard() {
10      try {
11        const response = await fetch('/api/dashboard-data');
12        const dashboardData = await response.json();
13        setData(dashboardData);
14      } catch (error) {
15        console.error('Failed to load dashboard:', error);
16      } finally {
17        setLoading(false);
18      }
19    }
20    
21    loadDashboard();
22  }, []);
23  
24  if (loading) {
25    return <DashboardSkeleton />;
26  }
27  
28  return (
29    <div className="dashboard">
30      <RealTimeMetrics data={data} />
31      <LiveCharts data={data} />
32    </div>
33  );
34}
35

Hybrid Approaches


Sometimes the best solution combines multiple rendering strategies:

Code

1// pages/product/[id].tsx
2export async function getStaticProps({ params }) {
3  // Get static product data at build time
4  const product = await fetchProduct(params.id);
5  
6  return {
7    props: {
8      product,
9      // Pass configuration for client-side fetching
10      realtimeConfig: {
11        endpoint: `/api/products/${params.id}/realtime`,
12        pollInterval: 5000
13      }
14    },
15    revalidate: 3600
16  };
17}
18
19function ProductPage({ product, realtimeConfig }) {
20  // Use static data for initial render
21  const [data, setData] = useState(product);
22  
23  // Set up real-time updates
24  useEffect(() => {
25    const interval = setInterval(async () => {
26      const response = await fetch(realtimeConfig.endpoint);
27      const updates = await response.json();
28      setData(prev => ({ ...prev, ...updates }));
29    }, realtimeConfig.pollInterval);
30    
31    return () => clearInterval(interval);
32  }, [realtimeConfig]);
33  
34  return (
35    <div className="product">
36      <h1>{data.name}</h1>
37      <RealTimeInventory stock={data.stock} />
38      <DynamicPricing price={data.price} />
39    </div>
40  );
41}
42

Each rendering strategy in the Pages Router has its strengths and ideal use cases. Understanding these patterns allows for choosing the most appropriate approach for different parts of your application, balancing factors like performance, data freshness, and user experience.

Node.js and Edge Runtimes

When developing applications with Next.js, developers have a choice between two primary execution environments or "runtimes": the Node.js Runtime and the Edge Runtime.

Each runtime offers distinct capabilities, advantages, and limitations crucial to understanding effective application architecture. This sub-chapter provides a detailed comparison of the two runtimes, helping developers make informed decisions based on their specific project needs.


Understanding Runtimes in Next.js

  • Node.js Runtime: This is the default runtime in Next.js, which provides full access to all Node.js APIs and the rich ecosystem of npm packages compatible with Node.js. This runtime is best for applications requiring comprehensive server-side capabilities, including file system access, custom server configurations, and the utilization of a vast array of third-party npm modules.
  • Edge Runtime: Introduced to cater to high-performance, low-latency applications, the Edge Runtime is a lightweight, restricted environment primarily based on Web APIs. It's designed to run small, simple functions that benefit from being closer to the user, thus minimizing latency.


Key Differences and Considerations

  1. Initialization Speed and Cold Boot Time:
  • Node.js: Has a normal startup time as it initializes a more extensive runtime environment.
  • Edge: Features a very low cold boot time, ideal for performance-critical applications that require quick starts.
  1. Input/Output Operations:
  • Node.js: Supports all I/O operations available in Node.js, including file system operations and external database connections.
  • Edge: Limited to I/O operations that can be performed over the network using fetch. It does not support Node.js-specific APIs like fs for file system access.
  1. Scalability:
  • Node.js: Scalable but often requires more infrastructure management unless deployed on serverless platforms that abstract away these complexities.
  • Edge: This type of infrastructure offers the highest scalability with minimal overhead and is often managed by cloud providers that distribute edge functions geographically.
  1. Security and Isolation:
  • Both runtimes offer high levels of security. However, the Edge runtime often runs in a more controlled environment, which can inherently reduce the surface area for attacks.
  1. Latency:
  • Node.js: Node.js: Normal latency is suitable for most applications but not optimized for ultra-low latency requirements.
  • Edge: This method provides the lowest latency by executing code geographically closer to the user, significantly improving response times for global applications.
  1. Support for npm Packages:
  • Node.js: Supports virtually all npm packages.
  • Edge: Supports only a subset of npm packages compatible with Web APIs, limiting the use of many traditional Node.js packages.
  1. Rendering Capabilities:
  • Both runtimes support dynamic and static rendering, although the Edge runtime does not support static generation during build time.


Choosing the Right Runtime:

  • Use Node.js Runtime when your application requires access to the full Node.js API, needs to perform complex computations, or interacts heavily with a filesystem or external databases.
  • Opt for Edge Runtime to deliver dynamic content with minimal latency, especially when the application logic is simple and can be executed within the Edge environment's constraints (like package size and API limitations).

Keypoints

  • Rendering Strategies Overview: Traditional approaches (SSR, CSR, ISR) have evolved with the Next.js app directory, introducing client and server components for granular control.
  • Dual Execution Advantages: Improves user experience by quickly rendering pages on the server and retaining interactivity on the client.
  • Server-Side Rendering Limitations: Some functionalities, such as browser-specific APIs and specific React hooks, cannot be executed on the server.
  • Impact on SEO: Server-side rendering allows search engines to crawl content easily, improving SEO.
  • Static Site Generation (SSG): Pre-renders HTML at build time, resulting in faster page load times but potentially longer build times for large sites.
blazity comet

Get a quote

Need help with Next.js?
Look no further!
Brian Grafola, CTO at Vibes

“We are very happy with the outcomes and look forward to continuing to work with them on larger initiatives.”

Brian Grafola

CTO at Vibes

Trusted by
Solana logoVibes logoArthur logoHygraph logo
The contact information is only used to process your request. By clicking send, you agree to allow us to store information in order to process your request.