Next.js rendering strategies
Next.js offers several strategies for generating a page's HTML. Each strategy determines when the HTML is produced: at build time, on every request, or incrementally. The choice of strategy directly impacts load time, content freshness, and hosting costs.
Static Site Generation (SSG)
Static generation produces the HTML at build time (next build). The resulting HTML files are deployed to a CDN and served directly to the browser without any server-side processing on each request.
How it works
- At build time, Next.js runs the data-loading functions
- The data is injected into the React components
- The HTML is generated and stored as static files
- On each request, the CDN serves the pre-generated HTML file
Implementation with the App Router
In the App Router, a page is static by default if it doesn't depend on any dynamic data. For a page that consumes the WordPress API:
// app/blog/page.tsx — Static page generated at build time
export default async function BlogPage() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts?per_page=20&_embed`,
{ cache: 'force-cache' } // Explicit static behavior
);
const posts = await res.json();
return (
<main>
<h1>Articles</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
</article>
))}
</main>
);
}
For dynamic routes, generateStaticParams tells Next.js which parameters to pre-generate:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts?per_page=100`
);
const posts = await res.json();
return posts.map((post: any) => ({ slug: post.slug }));
}
When to use SSG
- Pages whose content rarely changes (legal notices, About, FAQ)
- Blog posts published without frequent edits
- Category and archive pages
< 50ms
TTFB (Time to First Byte)
The HTML file is served directly from the CDN, with no server processing
0
Server cost per request
No server execution — static files are served by the CDN
Build
Content freshness
Content is up to date as of the latest build only
Server-Side Rendering (SSR)
Server-side rendering generates HTML on every request. The Next.js server queries the WordPress API, runs the React components, and returns the HTML to the browser.
Implementation
To force SSR, use the cache: 'no-store' option on fetch calls, or export dynamic = 'force-dynamic':
// app/search/page.tsx — Page rendered on every request
export const dynamic = 'force-dynamic';
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const query = searchParams.q ?? '';
const res = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts?search=${query}&_embed`,
{ cache: 'no-store' }
);
const posts = await res.json();
return (
<main>
<h1>Results for "{query}"</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
</article>
))}
</main>
);
}
When to use SSR
- Search pages whose content depends on query parameters
- Pages personalized for the logged-in user
- Pages displaying real-time data (stock, prices, availability)
Incremental Static Regeneration (ISR)
ISR combines static performance with dynamic freshness. Pages are generated at build time like in SSG, then regenerated in the background after a configured interval.
Detailed mechanics
Initial build
At build time, Next.js generates static pages and deploys them to the CDN. These pages are served to visitors.
First request after expiration
When a visitor accesses a page whose revalidation interval has expired, they receive the cached version (stale). In the background, Next.js triggers regeneration.
Background regeneration
Next.js queries the WordPress API, generates the new HTML, and replaces the cached version. This process is invisible to the visitor.
Subsequent requests
Following visitors receive the regenerated version. The cycle restarts at the next interval expiration.
This mechanism is called stale-while-revalidate: the visitor always receives an immediate response (the cached version), and the update happens in the background.
Implementation
// app/blog/[slug]/page.tsx — Static page with ISR revalidation
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts?slug=${params.slug}&_embed`,
{ next: { revalidate: 3600 } } // Regeneration every hour
);
const posts = await res.json();
const post = posts[0];
return (
<article>
<h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</article>
);
}
On-demand revalidation
Rather than waiting for the interval to expire, a WordPress webhook can trigger regeneration immediately:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
const body = await request.json();
const slug = body.post_name;
revalidatePath(`/blog/${slug}`);
revalidatePath('/blog'); // Also regenerate the listing page
return Response.json({ revalidated: true, slug });
}
On the WordPress side, a plugin or publish_post hook sends a POST request to this route on each publication.
Client-side data loading
For data that changes frequently and doesn't need SEO indexing (comments, view count, login status), client-side loading is preferable.
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export function CommentSection({ postId }: { postId: number }) {
const { data, error, isLoading } = useSWR(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/comments?post=${postId}`,
fetcher,
{ refreshInterval: 30000 } // Refresh every 30 seconds
);
if (isLoading) return <p>Loading comments...</p>;
if (error) return <p>Loading error</p>;
return (
<section>
<h3>{data.length} comment(s)</h3>
{data.map((comment: any) => (
<div key={comment.id}>
<strong>{comment.author_name}</strong>
<div dangerouslySetInnerHTML={{ __html: comment.content.rendered }} />
</div>
))}
</section>
);
}
Decision matrix
Hybrid approach: combining strategies
One of the advantages of Next.js is the ability to combine multiple strategies in a single project. Each route can use its own strategy.
app/
page.tsx → SSG (static home page)
blog/
page.tsx → ISR revalidate: 3600 (post listing)
[slug]/
page.tsx → ISR revalidate: 3600 (post detail)
search/
page.tsx → SSR (dynamic search results)
account/
page.tsx → SSR (personalized content)
Practical rule for strategy selection
For a standard headless WordPress site, ISR with on-demand revalidation is the recommended default choice. It covers most use cases (posts, pages, categories) by combining static performance with content freshness. Reserve SSR for pages that depend on query parameters or the logged-in user, and pure SSG for pages whose content only changes between deployments.
Performance impact
50ms
TTFB with SSG/ISR
Pre-generated pages are served from the CDN closest to the visitor
200-500ms
TTFB with SSR
The server generates the HTML on every request — time depends on WordPress API latency
1-3s
TTFB with CSR
The browser loads the JavaScript, then makes API calls before displaying content
Key takeaways
Key points
-
SSG generates HTML at build time. Maximum performance, but content is only updated after a new build.
-
SSR generates HTML on every request. Always up-to-date content, but higher response time and server cost proportional to traffic.
-
ISR combines SSG and periodic revalidation. The recommended default strategy for a headless WordPress site.
-
The hybrid approach lets you use the strategy best suited to each route within a single project.
-
On-demand revalidation via webhook is the most efficient method to sync WordPress content with the Next.js frontend.
Next.js for headless WordPress
Article suivantMedia management and image optimization