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).
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:
- Streaming, Suspense & Hydration
- Third-Party Scripts
- Rendering Strategies
- Infrastructure
- Real-life Examples
- How to Measure Web Performance
- Bundle Analysis.
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.
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:
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.
In this chapter, we’ll explore:
We will do so using code implementation, as code says more than a thousand words.
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}
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
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
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 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
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
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
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.
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.
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:
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 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 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:
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) 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-side rendering in App Router:
The Pages Router in Next.js provides three main rendering strategies:
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.
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 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
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
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.
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
Key Differences and Considerations
Choosing the Right Runtime:
Get a quote
“We are very happy with the outcomes and look forward to continuing to work with them on larger initiatives.”
Brian Grafola
CTO at Vibes