Skip to content
Go To Agency
Journal

Développement Web · May 27, 2026 · updated October 17, 2026 · 8 min read

Next.js 15 and 16 for e-commerce: App Router patterns that hold up in production

Static product pages with timed and event-based refresh, a cart built on useOptimistic, Server Actions versus route handlers for payment, and what Next.js 16 changed for stores.

In short

  • Next.js 15 (October 2024) stopped caching fetch, GET route handlers and client navigations by default; Next.js 16 (October 2025) made Turbopack the default and moved partial prerendering into Cache Components.
  • Product pages: prerender best sellers, render the long tail on first visit, refresh on a timer or on a signed webhook.
  • Cart: Server Actions with useOptimistic. Payment: route handlers with idempotency keys. Every Server Action is a public endpoint.
  • Measure Core Web Vitals on your own product pages with real visitors: the framework does not make a store fast on its own.

A store is the kind of site where the App Router's choices matter most: thousands of product pages that should be static, a cart that must feel instant, a checkout that must never fail silently. This guide goes through the patterns that hold up, version by version, with what changed in Next.js 15 and again in Next.js 16. Release dates and behaviours come from the Next.js release notes, linked in each section.

There are no benchmark figures here on purpose. How fast a store gets depends on its catalogue, its hosting and above all its third-party scripts, so the only numbers worth trusting are the ones you measure on your own product pages, with real visitors. Our Next.js vs WordPress comparison shows why: in real-user data, Next.js sites as a whole pass Core Web Vitals on mobile less often than WordPress sites, mostly because of the JavaScript they ship.

Where Next.js stands in 2026

  • Next.js 15 (21 October 2024) made request APIs such as cookies(), headers() and params asynchronous, stopped caching fetch requests, GET route handlers and client navigations by default, and declared Turbopack stable for next dev.
  • Next.js 15.3 (9 April 2025) brought Turbopack to production builds, in alpha.
  • Next.js 16 (21 October 2025) made Turbopack stable and the default bundler for development and builds, introduced Cache Components (the model that now carries partial prerendering), removed the experimental ppr flag, renamed middleware.ts to proxy.ts and changed the signature of revalidateTag().

Most stores in production run one of these two majors. The patterns below work on both; where they differ, it is said.

Moving an existing store to the App Router

The app and pages directories can run side by side, which lets a store migrate route by route. The usual order is the one that brings the most for the least risk: product and category pages first, because they are mostly read-only and gain the most from Server Components; the cart next; checkout last, or never.

The trap is translation. getServerSideProps and getStaticProps have no one-to-one equivalent. Code ported line by line from the Pages Router tends to work and to ship far more client JavaScript than needed, because whole trees end up behind "use client". The data flow has to be redesigned: fetch on the server, pass plain data down, keep client components small and close to the interaction that needs them.

A cart that feels instant and stays true

The cart is client state that has to start from server data. React 19's useOptimistic, combined with a transition, shows the change immediately while the server stays the source of truth:

// app/cart/cart-provider.tsx
'use client';

import { createContext, useContext, useOptimistic, useTransition } from 'react';
import { addToCartAction } from './actions';

type CartItem = { id: string; sku: string; qty: number; price: number };
type CartState = { items: CartItem[] };

function useCartLogic(initial: CartState) {
  const [isPending, startTransition] = useTransition();
  const [cart, applyOptimistic] = useOptimistic(
    initial,
    (state, item: CartItem) => ({ items: [...state.items, item] })
  );

  function add(item: CartItem) {
    startTransition(async () => {
      applyOptimistic(item);
      await addToCartAction(item);
    });
  }

  return { cart, add, isPending };
}

const CartContext = createContext<ReturnType<typeof useCartLogic> | null>(null);

export function CartProvider({ children, initial }: { children: React.ReactNode; initial: CartState }) {
  return <CartContext.Provider value={useCartLogic(initial)}>{children}</CartContext.Provider>;
}

export const useCart = () => {
  const ctx = useContext(CartContext);
  if (!ctx) throw new Error('useCart must be used inside CartProvider');
  return ctx;
};

The provider receives initial from a Server Component that reads the cart. Keep that read inside the part of the layout that needs it: a cart badge that reads a cookie at the top of the root layout makes every page of the store dynamic.

Product pages: static first, refreshed on purpose

A product page changes far less often than it is viewed. Rendering it on every request wastes server time and slows the first byte. The pattern that fits most catalogues: prerender the pages that matter most at build time, render the long tail on first visit, cache everything, and refresh on a timer or on an event.

// app/products/[slug]/page.tsx
export const revalidate = 60;       // refresh at most once a minute
export const dynamicParams = true;  // render unknown slugs on first visit

export async function generateStaticParams() {
  const top = await getTopProducts(500); // your best sellers, not the whole catalogue
  return top.map((p) => ({ slug: p.slug }));
}

export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const product = await getProductBySlug(slug);
  if (!product) notFound();
  return <ProductDetail product={product} />;
}

Stock levels and prices that must be exact go in a small dynamic piece of the page, not in the whole page. If you turn on Cache Components in Next.js 16, the same idea is written with the "use cache" directive and cache lifetimes instead of route-level revalidate.

Refreshing on an event

When a price changes in the back office, waiting for the timer is not good enough. A signed webhook can invalidate the page and the tagged data behind it:

// app/api/webhooks/product-updated/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { verifyWebhookSignature } from '@/lib/webhooks';

export async function POST(req: Request) {
  const body = await req.text();
  if (!verifyWebhookSignature(body, req.headers.get('x-signature'))) {
    return new Response('Invalid signature', { status: 401 });
  }
  const { slug, sku } = JSON.parse(body);

  revalidatePath(`/products/${slug}`);
  // Next.js 15: revalidateTag(`product:${sku}`)
  // Next.js 16: a cacheLife profile is required as second argument
  revalidateTag(`product:${sku}`, 'max');

  return Response.json({ ok: true });
}

In Next.js 16, the one-argument form of revalidateTag() is deprecated; inside a Server Action, updateTag() expires the data and reads it fresh in the same request, which suits an admin who saves a product and expects to see the change.

Server Actions: yes for the cart, think twice for payment

Server Actions are a good fit for mutations whose failure is cheap and visible: add to cart, change a quantity, toggle a wishlist item, subscribe to a newsletter. They remove a lot of plumbing.

Payment is different. Creating a payment intent or an order needs idempotency keys, structured errors, logs you can follow request by request, and the ability to reproduce a call with a plain HTTP client. A route handler gives you clean HTTP semantics for all of that. Whatever you choose, remember what the Next.js 15 release notes say plainly: a Server Action is a public HTTP endpoint, even if you only call it from your own components. Validate its inputs and check authorisation inside it.

Edge runtime and proxy

The Edge runtime suits small, read-only work close to the visitor: redirects, a cookie for an A/B test, a country check. It does not suit anything that needs Node.js APIs, such as the pg driver for PostgreSQL, which opens TCP connections. In Next.js 16, middleware.ts becomes proxy.ts and runs on Node.js; middleware.ts remains available for Edge use but is deprecated. If your store's database is PostgreSQL on your own server, keep pages and API routes on Node.js.

Partial prerendering, now Cache Components

Partial prerendering serves a static shell immediately and streams the dynamic holes (recommendations, cart count, recently viewed) into it, inside <Suspense> boundaries. It was first introduced in 2023 and stayed behind an experimental flag through Next.js 15. Next.js 16 removed that flag and the route-level experimental_ppr export: partial prerendering now comes with Cache Components, which you turn on in the config.

// next.config.ts (Next.js 16)
const nextConfig = {
  cacheComponents: true,
};

export default nextConfig;

With Cache Components, nothing is cached unless you ask for it with "use cache": dynamic code runs at request time by default. For a store, that means deciding explicitly which parts of the home and category pages can be cached, and wrapping the personalised parts in <Suspense>. The work is mostly an audit: finding the components that read cookies or headers without needing to.

Turbopack

Turbopack has been stable for next dev since Next.js 15, reached production builds in alpha with 15.3, and is the default bundler for both in Next.js 16. You can still build with webpack using next build --webpack. Before switching an existing store, check its custom webpack configuration: loaders have to be declared under the top-level turbopack key, and a plugin without a Turbopack equivalent keeps you on webpack for now.

Caching rules worth writing down

  1. State the cache behaviour of every server-side data call. Since Next.js 15, fetch is not cached by default. On Next.js 15 that means cache: 'force-cache' or next: { revalidate } where you want caching; with Cache Components, "use cache". Anything implicit is a review comment.
  2. Tag product, category and content data. Invalidating by tag is more precise than invalidating by path.
  3. Read cookies and headers as low in the tree as possible. Calling them makes the route render at request time. A stray headers() in a shared layout turns a static catalogue into a dynamic one.

When Next.js is the wrong choice for a store

If the store runs on Shopify or WooCommerce with a theme that covers what you sell, a headless Next.js front end is a second system to build and maintain for little gain. It starts to pay when you have:

  • catalogue logic a platform cannot express: configurators, bundles, B2B price tiers;
  • a lot of editorial content around the products;
  • several brands or markets served from one codebase;
  • search requirements beyond what the theme produces;
  • a developer who will own the front end after launch.

Without the last one, do not start.

Checklist before going live

  • Every server-side data call states its cache behaviour.
  • Product pages are static with timed or event-based refresh; only stock and price are dynamic.
  • Best sellers are prerendered at build time, the long tail on first visit.
  • The cart uses Server Actions with useOptimistic; payment goes through route handlers with idempotency keys.
  • Every Server Action validates its inputs and checks authorisation.
  • No cookies() or headers() call in shared layouts unless it is needed there.
  • On Next.js 16: revalidateTag() calls carry a cache profile, middleware has moved to proxy.ts.
  • Core Web Vitals are measured on product pages with real visitors (Search Console or CrUX), not only in Lighthouse.

Frequently asked questions

Is Partial Prerendering stable in Next.js?

It no longer exists as a separate flag. Partial prerendering stayed experimental through Next.js 15; Next.js 16, released on 21 October 2025, removed the experimental ppr flag and delivers partial prerendering through Cache Components, turned on with cacheComponents: true in next.config.

Is Turbopack ready for production builds?

Yes since Next.js 16 (21 October 2025), where it is stable and the default bundler for development and builds. In Next.js 15.3 it was in alpha for builds. Projects with custom webpack loaders or plugins should check compatibility first; next build --webpack keeps webpack.

Should I use Server Actions for checkout?

They are a good fit for cart mutations. For payment and order creation, route handlers give clean HTTP semantics, idempotency keys and request-level logs, which is what you need to debug a failed payment. Either way, treat each Server Action as a public HTTP endpoint and validate its inputs.

What changed in caching between Next.js 14 and 15?

Next.js 15 stopped caching fetch requests, GET route handlers and client navigations by default, and made cookies(), headers() and params asynchronous. Caching is now something you declare: force-cache or revalidate on Next.js 15, the use cache directive with Cache Components on Next.js 16.

Read next

  1. August 25, 2026E-commerce checkout conversion in 2026: express payments, BNPL and where carts are actually lostE-commerce · 11 min read
  2. August 10, 2026Per-Seat SaaS vs Self-Hosted Postgres: The Arithmetic Nobody Runs HonestlyDéveloppement Web · 13 min read
  3. June 10, 2026Next.js vs WordPress in 2026: what real-user data says, and which to chooseDéveloppement Web · 10 min read

Is your project decided?

Present it in writing. We read everything, then we answer.

Pitch a project