Next.js Tutorial: Partial Prerendering for Optimal Performance

Next.js is a powerful React framework. It enables developers to build fast, SEO-friendly web applications. With version 13 and beyond, Next.js introduced a game-changer — Partial Prerendering (PPR).

This tutorial will guide you through Partial Prerendering. It explains what it is, why it matters, and how to use it.

What is Partial Prerendering?

Traditionally, pages in Next.js are either static or dynamic. Static pages are fast. Dynamic pages are flexible but slower.

Partial Prerendering offers the best of both. It allows you to serve static HTML immediately. Then, it hydrates dynamic content afterward.

In simple terms, it displays information to the user quickly. Then it loads the rest seamlessly. It combines performance with interactivity.

Why Use Partial Prerendering?

Speed matters. Google ranks fast-loading pages higher. Users expect instant feedback. A one-second delay can reduce conversions by as much as 7%.

With Partial Prerendering, your site becomes faster. Moreover, you can avoid loading spinners. Instead, serve meaningful HTML instantly.

Also, Partial Prerendering is perfect for personalizing content. For example, you can render a product catalog statically. Then, inject user-specific pricing dynamically.

Setting Up a Next.js Project

First, create a new Next.js project. Run:

Creating a Page with Partial Prerendering

Navigate to the app directory. Create a new file: app/dashboard/page.tsx.

Here’s a simple example:

export const dynamic = 'force-static'; // This triggers PPR

export default function DashboardPage() {
  return (
    <div>
      <h1>Welcome to Your Dashboard</h1>
      <UserProfile />
    </div>
  );
}

In the above code, the DashboardPage renders statically. The component UserProfile will be hydrated later.

Next, create UserProfile.tsx:

'use client'

import { useEffect, useState } from 'react';

export default function UserProfile() {
  const [name, setName] = useState('Loading...');

  useEffect(() => {
    fetch('/api/user')
      .then((res) => res.json())
      .then((data) => setName(data.name));
  }, []);

  return <p>Hello, {name}!</p>;
}

This component fetches data on the client side. It’s dynamic and personal, but doesn’t block the initial render.

Creating the API Route

Now, create a file under app/api/user/route.ts:

export async function GET() {
  return new Response(JSON.stringify({ name: 'John Doe' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

With this, your app serves fast static content. Then, it personalizes the page with client-side data.

When to Use Partial Prerendering

Partial Prerendering is ideal in several cases:

  • Personalized dashboards — Static layout + dynamic user data.
  • Product pages — Static description + dynamic pricing or inventory.
  • News portals — Static articles + dynamic related content.

Still, you should not use it for secure or sensitive content. Client-side hydration may expose private data if not handled carefully.

Benefits for SEO

Search engines love speed. Partial Prerendering boosts performance. Static HTML gets indexed quickly.

Also, your structured data remains intact. This increases the chances of getting rich results on Google.

You still control what appears in the initial HTML. Use <meta> tags wisely. Add schema markup if necessary.

Performance Tips

To maximize performance:

  • Split the code into dynamic components.
  • Cache API responses when possible.
  • Use React Suspense for a smoother UX.
  • Monitor web vitals using tools like Vercel Analytics or Lighthouse.

Every millisecond counts. Optimization is not optional — it’s a strategic necessity.

Common Mistakes to Avoid

Even seasoned developers make mistakes. Avoid these pitfalls:

  • Hydrating too much on the client side.
  • Forgetting to mark dynamic components with 'use client'.
  • Misusing dynamic = 'force-dynamic' when not necessary.

Always profile your app. Tools like React DevTools and Next.js profiler help find bottlenecks.

Example Website Partial Prerendering

product pages using Partial Prerendering in Next.js 13+ with a static product description and dynamic pricing/inventory.

Directory Structure

/app
  /products
    /[id]
      page.tsx
    /api
      /pricing
        route.ts
/components
  DynamicPrice.tsx
/type
  ProductInfo.ts

1. Create app/products/[id]/page.tsx

This page statically renders the product description and defers pricing/inventory to the client:

export const dynamic = "force-static";

import DynamicPrice from "@/components/DynamicPrice";

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = {
    id: params.id,
  };

  return (
    <div>
      <DynamicPrice productId={product.id} />
    </div>
  );
}

2. Create components/DynamicPrice.tsx

This component fetches the dynamic data after render:

"use client";

import { useEffect, useState } from "react";
import {
  TagIcon,
  CubeIcon,
  CheckCircleIcon,
} from "@heroicons/react/24/outline";
import { ArrowPathIcon } from "@heroicons/react/24/outline";

export default function DynamicPrice({ productId }: { productId: string }) {
  const [price, setPrice] = useState("");
  const [stock, setStock] = useState("");

  useEffect(() => {
    fetch(`/products/api/pricing?productId=${productId}`)
      .then((res) => res.json())
      .then((data) => {
        setPrice(data.price);
        setStock(data.stock);
      });
  }, [productId]);

  if (!price)
    return (
      <div className="flex items-center justify-center space-x-2 text-gray-500 animate-pulse">
        <ArrowPathIcon className="w-5 h-5 animate-spin text-indigo-500" />
        <span className="text-sm font-medium">Loading price...</span>
      </div>
    );

  return (
    <div className="max-w-sm mx-auto mt-6 p-6 bg-white rounded-xl shadow-md space-y-4 border border-gray-100">
      <div className="flex items-center space-x-2">
        <CubeIcon className="w-6 h-6 text-indigo-600" />
        <p className="text-lg font-semibold text-gray-800">
          Product: {productId}
        </p>
      </div>

      <div className="flex items-center space-x-2">
        <TagIcon className="w-6 h-6 text-green-500" />
        <p className="text-md text-gray-700">
          <strong>Price:</strong> ${price}
        </p>
      </div>

      <div className="flex items-center space-x-2">
        <CheckCircleIcon className="w-6 h-6 text-blue-500" />
        <p className="text-md text-gray-700">
          <strong>In Stock:</strong> {stock}
        </p>
      </div>
    </div>
  );
}

3. Create app/products/api/pricing/route.ts

The API route serves dynamic data:

import { DynamicData } from "../../../../types/ProductInfo";
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const productId = searchParams.get("productId");

  // Simulate dynamic pricing/inventory
  const dynamicData: DynamicData = {
    "coffee-maker": { price: "129.99", stock: "12" },
    "smart-toaster": { price: "89.99", stock: "8" },
  };

  const fallback = { price: "49.99", stock: "Out of stock" };
  const data = dynamicData[productId || ""] || fallback;

  return new Response(JSON.stringify(data), {
    headers: { "Content-Type": "application/json" },
  });
}

4. Create type/ProductInfo.ts

export type ProductInfo = {
  price: string;
  stock: string;
};

export type DynamicData = {
  [productId: string]: ProductInfo;
};

5. Run the App

Start the development server:

npm run dev

Open your browser and go to http://localhost:3000.
You should see the default Next.js page.

Home page

Test products page

1. Search for price by ID.

http://localhost:3000/products/coffee-maker
coffee-maker

2. Search for price by ID.

http://localhost:3000/products/smart-toaster
smart-toaster

3. Search for a price by a non-existent ID.

http://localhost:3000/products/oven
oven

Error: Module not found

Error: Module not found: @heroicons/react.

Solution: Install the module.

npm install @heroicons/react

Error: CanaryOnlyError

Message

[CanaryOnlyError: The experimental feature "experimental.ppr" can only be enabled when using the latest canary version of Next.js.]

Solution: Upgrade to the Canary Version of Next.js

npm install next@canary

Verify Version: In your package.jsonThe Next.js version should look something like:

"next": "^15.4.0-canary.83",

Finally

Next.js continues to evolve. Partial Prerendering is a breakthrough. It balances speed and interactivity beautifully.

Use it wisely. Identify which parts of your site can be static. Delegate dynamic behavior to isolated components.

This approach improves SEO, user experience, and performance.

Start small. Then scale up. Soon, you’ll master Partial Prerendering — and your users will thank you.

This article was originally published on Medium.

Leave a Comment

Your email address will not be published. Required fields are marked *