Maximizing Performance with Advanced Rendering Strategies
The difference between a good Next.js app and a great one is choosing the right rendering strategy for each route based on how frequently the data changes and how important speed is.
Incremental Static Regeneration in Depth
ISR combines the speed of static HTML with the freshness of server rendering:
// app/blog/page.tsx
export const revalidate = 3600; // Regenerate at most once per hour
export default async function BlogPage() {
// This fetch is cached for 1 hour
const posts = await db.post.findMany({
where: { published: true },
orderBy: { publishedAt: 'desc' },
take: 20,
});
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
On-demand ISR — trigger revalidation from a webhook when data changes:
// app/api/revalidate/route.ts
import { NextRequest } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
revalidatePath('/blog');
revalidateTag('posts');
return Response.json({ revalidated: true, slug: body.slug });
}
Call this endpoint from your CMS or database webhook when content is published.
Partial Prerendering
PPR is the most advanced rendering mode — it sends a static shell instantly while streaming in dynamic parts:
// next.config.ts
const nextConfig: NextConfig = {
experimental: { ppr: true },
};
// app/blog/[slug]/page.tsx
export const experimental_ppr = true;
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug); // Prerendered at build time
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
{/* These stream in dynamically */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments postId={post.id} />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedPosts category={post.categoryId} />
</Suspense>
</article>
);
}
Measuring Core Web Vitals
// app/layout.tsx
export function reportWebVitals(metric: any) {
switch (metric.name) {
case 'LCP': // Largest Contentful Paint — target < 2.5s
case 'FID': // First Input Delay — target < 100ms
case 'CLS': // Cumulative Layout Shift — target < 0.1
case 'FCP': // First Contentful Paint
case 'TTFB': // Time to First Byte
// Send to your analytics service
console.log(metric);
}
}
Core Web Vitals targets:
- LCP < 2.5 seconds (use
priorityon hero images) - CLS < 0.1 (use
next/font, explicit image dimensions) - FID/INP < 100ms (minimize client-side JavaScript)
The combination of ISR for content pages, PPR for mixed static/dynamic pages, and proper image/font optimization is what separates Next.js apps with 90+ Lighthouse scores from the average.