Skip to main content

ISR, Partial Prerendering, and Core Web Vitals

28/28
Chapter 7 Deployment and Performance

ISR, Partial Prerendering, and Core Web Vitals

22 min read Lesson 28 / 28

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 priority on 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.