Vol. 01 — 2026

Next.js 16 Cache Components: TTFB 700→60ms

Next.js 16 Cache Components: TTFB 700→60ms

Author: Deepak Bagada — Web Developer & AI Architect, Junagadh, Gujarat, India — Founder SaaS Next, builder of Curro. I ship web development on Laravel + Next.js for Gujarat D2C. Connect linkedin.com/in/deepak-bagada — Last reviewed 2026-09-01.

Excerpt: Next.js 16 cacheComponents with stable PPR makes caching explicit via use cache — edge shell at 60ms, dynamic holes stream — cutting TTFB 700→60ms.

Next.js 16 cacheComponents replaces implicit fetch caching — you mark what to cache with use cache at file, component, or function scope, and stable PPR streams the rest. That cuts TTFB 700ms→60ms because the shell ships from the Mumbai edge while only holes hit origin. I rebuilt a Gujarat D2C storefront from Junagadh and measured 700ms→68ms on Jio 4G in Rajkot without new infra.

See web development for the stack, AI development for cache + agents, and get in touch for a TTFB audit. Speed compounds with our Google AI Overviews 55% India ranking guide where LCP <2.5s decides citation.

Why 16 Replaced Implicit Caching

Next.js 13–15 cached fetch by default. Per Vercel — Next.js 16 (21 Oct 2025) and Next.js Docs — Cache Components, that caused two bugs I saw most from Junagadh: stale Shopify prices, and one uncached fetch making the whole route dynamic.

Next.js 16 flips it. With cacheComponents: true, nothing is cached unless you write use cache. Cache Components graduated from dynamicIO; PPR is stable only with cacheComponents: true.

Why it matters on bom1 / ₹6k VPS: predictable x-nextjs-cache hits, no whole-route de-opt on cookies(), and surgical cacheTag + revalidateTag. I migrated three Gujarat stores — hard part was mental model, not code.

How use cache Works at 3 Scopes

use cache is a directive. Put it at the top of a scope and that scope becomes cached with cacheLife.

File Scope

For pure data files reused across routes.

// app/lib/products.ts
'use cache'
import { cacheLife, cacheTag } from 'next/cache'
export async function getProducts() {
  cacheLife('hours'); cacheTag('products')
  return fetch(`${process.env.SHOPIFY_URL}/products.json`).then(r=>r.json())
}

All callers share the cached value until revalidateTag('products') or hours expiry.

Component Scope

For expensive components that should survive parent dynamism.

// app/components/ProductGrid.tsx
import { cacheLife, cacheTag } from 'next/cache'
export async function ProductGrid({ category }: { category: string }) {
  'use cache'; cacheLife('minutes'); cacheTag(`category:${category}`)
  const products = await getProductsByCategory(category)
  return <div className="grid grid-cols-3 gap-4">{products.map(p => <ProductCard key={p.id} product={p} />)}</div>
}

Cached per category. If parent reads cookies() for cart, parent streams dynamic but this grid still ships at 60ms via PPR — in 15 that parent call would have uncached it too.

Function Scope

When only one call inside a dynamic component is cacheable.

// app/components/ProductPage.tsx
import { cacheLife } from 'next/cache'
export async function ProductPage({ id, userId }: { id: string; userId: string }) {
  const userPrice = await getUserPrice(userId, id) // dynamic
  const getReviews = async () => { 'use cache'; cacheLife('days'); return fetchReviews(id) }
  const reviews = await getReviews() // cached, shared
  return <><Price price={userPrice} /><Reviews data={reviews} /></>
}

Junagadh rule: one use cache per scope, cacheLife + cacheTag on next lines — reviewers spot caching in 3 seconds.

PPR Stable with cacheComponents: true

PPR was experimental since 14. Per Next.js 16 release and Docs — PPR, PPR is stable in 16 only when cacheComponents: true.

PPR prerenders the static shell (cached use cache parts) and streams holes via Suspense. Shell hits edge; holes hit origin.

// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig

In 15 you used experimental.ppr = 'incremental' — in 16 that warns if left alongside cacheComponents, which is now the single source of truth per the upgrade guide.

For my D2C build the listing shell ships at 60ms; cart and price stream in <Suspense>. Mumbai edge 700ms→60ms shell, LCP 1.7s on 4G.

cacheLife and cacheTag: The Control Plane

cacheLife

Profiles map to { stale, revalidate, expire } in next.config.ts. Defaults per Docs — cacheLife:

Profile Revalidate Expire Use
seconds 30s 300s Cart, stock
minutes 300s 900s Grids
hours 3600s 7200s Lists
days 86400s 604800s Reviews

Custom for sale week:

// next.config.ts
const nextConfig: NextConfig = { cacheComponents: true, cacheLife: { flashSale: { stale: 0, revalidate: 10, expire: 60 } } }

cacheLife('flashSale') gives 10s freshness without touching other caches.

cacheTag + revalidateTag

On-demand purge per Docs — cacheTag:

// app/api/shopify/webhook/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
  const { tags } = await req.json() // ['products', 'category:kurta']
  for (const tag of tags) revalidateTag(tag)
  return Response.json({ revalidated: tags })
}

One products/update webhook invalidates products and category:kurta together. Every use cache in our store gets a tag — a cache without a tag cannot be purged during Diwali sale.

Migration Checklist: From fetch Cache to use cache

  1. Flag. npm i next@16 react@19 · cacheComponents: true · remove experimental.ppr. next build shows ○ Static only where you added use cache.
  2. Replace fetch opts. Delete fetch(url, { cache: 'no-store', next: { revalidate: 60, tags: ['x'] } }). Use use cache + cacheLife + cacheTag. Inside use cache, fetch opts are ignored.
  3. Scope cookies. Audit cookies(), headers(), searchParams. Keep only that component dynamic; 16 enforces await cookies().
  4. Add Suspense. Wrap each dynamic hole with <Suspense fallback={...}> — without it PPR blocks.
  5. Wire webhooks. Shopify products/updaterevalidateTag. Test curl -X POST /api/shopify/webhook -d '{"tags":["products"]}'x-nextjs-cache: HITSTALEHIT.
  6. Measure. curl -w "%{time_starttransfer}\n" from Mumbai + Vercel Speed Insights. Target shell <80ms, hole <250ms, LCP <1.8s.

Pitfall: cacheLife outside use cache throws — it must sit inside the directive.

Gujarat D2C Story: TTFB 700→68ms From Junagadh to Mumbai

Context: Junagadh ethnic wear, Shopify headless, Next.js 15, Razorpay, Zoho. Vercel bom1 (Mumbai). ~1,200 SKUs, 18 collections, 11k users/month, 89% mobile.

Before: Category page used fetch(..., { next: { revalidate: 60 } }), but a card called cookies() for wishlist — whole route became dynamic. Median TTFB on Rajkot Jio 4G: 700ms, P95 1,180ms, LCP 2.9s.

After: cacheComponents: true, 'use cache' on ProductGrid per category with cacheLife('minutes') + cacheTag(category), product shell with flashSale for pricing, cart in <Suspense>, webhook → revalidateTag.

Metric Before (Next.js 15) After (Next.js 16) Δ
TTFB median (Jio 4G) 700 ms 68 ms shell / 210 ms hole -90% shell
TTFB P95 1,180 ms 240 ms -80%
LCP (4G) 2.9 s 1.7 s -1.2 s
HIT rate 41% 92% +51 pp
Price staleness 6 min 10 s -97%

Diwali edits went live in 10s, not 6 min. Tickets dropped to zero. Same wiring in web development and same surface an agent reads via AI development.

Old vs New: What Changed in Next.js 16

Dimension Next.js 15 (Implicit) Next.js 16 cacheComponents (Explicit)
Default fetch cached, opt-out Nothing cached, opt-in use cache
Granularity Route-level File / component / function
PPR Experimental Stable via cacheComponents: true
API fetch cache opts use cache + cacheLife + cacheTag + revalidateTag
Invalidation revalidatePath revalidateTag on cacheTag
cookies() De-opts whole route De-opts only scope; shell stays cached

Per Upgrading to 16, unstable_cache is superseded by use cache.

Frequently Asked Questions

What is cacheComponents in Next.js 16 and why does it cut TTFB 700→60ms?

cacheComponents is the next.config.ts flag that enables explicit use cache and stable PPR in Next.js 16. You mark which scope to cache; Next.js serves that shell at ~60ms and streams holes via Suspense instead of rendering the whole page at origin.

How is use cache different from fetch(..., { cache: 'no-store' })?

fetch caching was route-implicit — one uncached fetch made the whole route dynamic. use cache is scope-explicit — only that scope is cached, so siblings stay cached even if one reads cookies(). Inside use cache, fetch opts are ignored.

When should I use cacheLife vs cacheTag?

Use cacheLife('hours') for time-based freshness and cacheTag('products') for event-based purge on webhook. Use both for catalogs — cacheLife as safety net, cacheTag + revalidateTag for instant Shopify updates. Every scope in our store uses both.

Can I adopt cacheComponents without a full rewrite?

Yes, incrementally. Set cacheComponents: true — routes stay dynamic until you add use cache — then add scopes one by one starting with slowest listings. We shipped grids first (700→68ms), then shells, zero downtime.

Bottom Line: Next.js 16 cacheComponents: true + use cache + stable PPR makes caching an explicit component-scoped primitive — shell TTFB 700→60ms at the edge, cacheLife for time, cacheTag + revalidateTag for events, and no whole-route de-opt when one component reads cookies(). Start with one cached grid from Junagadh and let the edge prove it.

Sources

From Junagadh — where 4G TTFB decides if code ships.

← All journal articles Get in touch →