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
- Flag.
npm i next@16 react@19·cacheComponents: true· removeexperimental.ppr.next buildshows○ Staticonly where you addeduse cache. - Replace fetch opts. Delete
fetch(url, { cache: 'no-store', next: { revalidate: 60, tags: ['x'] } }). Useuse cache+cacheLife+cacheTag. Insideuse cache, fetch opts are ignored. - Scope
cookies. Auditcookies(),headers(),searchParams. Keep only that component dynamic; 16 enforcesawait cookies(). - Add Suspense. Wrap each dynamic hole with
<Suspense fallback={...}>— without it PPR blocks. - Wire webhooks. Shopify
products/update→revalidateTag. Testcurl -X POST /api/shopify/webhook -d '{"tags":["products"]}'→x-nextjs-cache: HIT→STALE→HIT. - 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,cacheLifefor time,cacheTag+revalidateTagfor events, and no whole-route de-opt when one component readscookies(). Start with one cached grid from Junagadh and let the edge prove it.
Sources
- Vercel — Next.js 16 (21 Oct 2025) — nextjs.org/blog/next-16
- Next.js Docs —
use cache/cacheLife/cacheTag— nextjs.org/docs/app/api-reference/directives/use-cache - Next.js Docs — Upgrading to 16 — nextjs.org/docs/app/guides/upgrading/version-16
From Junagadh — where 4G TTFB decides if code ships.