spinny:~/writing $ less nextjs-16-cache-components.md
12For years one of the most annoying questions in Next.js has been: "Is this page static or dynamic?". It seems like a simple question, until you add a call to `cookies()`, a `fetch` with different options, a database client, a CMS, a shopping cart, or a piece of custom content.34Next.js 16 is interesting because it tries to make this conversation less mysterious. It doesn't eliminate complexity, but it shifts the mental model: routes are dynamic by default, the cache declares itself where needed, and `Suspense` becomes the natural way to compose fast shells with parts that stay fresh.56The feature to understand is Cache Components. Stable Turbopack, React Compiler, `proxy.ts`, and the new invalidation APIs are important, but they revolve around the same problem: building fast apps without having to guess what the framework decided behind the scenes.78## Because this thing matters910In a real app you don't just have "static pages" and "dynamic pages". You have different pieces with different needs.1112The product sheet may change a few times a day. The price may change more often. Availability must be almost live. The username is personal. Reviews can be streamed. The sidebar can be stable. The cart does not.1314If you treat everything as one unit, you always end up in one of two extremes:1516- aggressive caching and risk of seeing old data;17- dynamic rendering everywhere and performance worse than necessary.1819Cache Components serves precisely to avoid this false choice.2021## The model in practice2223With `cacheComponents: true`, you can declare what is cacheable using `"use cache"`. Then you can associate duration and tags with `cacheLife()` and `cacheTag()`. Dynamic parts remain dynamic and can be isolated with `Suspense`.2425```mermaid26flowchart TD27 Request[User request] --> Shell[Cached shell]28 Request --> Dynamic[Dynamic sections]29 Shell --> FirstPaint[First quick content]30 Dynamic --> Stream[Streaming inside Suspense]31 Stream --> Complete[Full page]32```3334The setup is small:3536```typescript37// next.config.ts38import type { NextConfig } from 'next';3940const nextConfig: NextConfig = {41 cacheComponents: true,42};4344export default nextConfig;45```4647The big change is not in the config. It's in how you start writing the components.4849```tsx50// app/products/[slug]/page.tsx51import { Suspense } from 'react';52import { cacheLife, cacheTag } from 'next/cache';5354async function getProduct(slug: string) {55 'use cache';5657 cacheLife('hours');58 cacheTag(`product:${slug}`);5960 return db.product.findUnique({ where: { slug } });61}6263async function ProductDetails({ slug }: { slug: string }) {64 const product = await getProduct(slug);6566 return (67 <section>68 <h1>{product.name}</h1>69 <p>{product.description}</p>70 </section>71 );72}7374async function LiveInventory({ slug }: { slug: string }) {75 const inventory = await db.inventory.findFirst({ where: { slug } });76 return <p>{inventory.quantity} pezzi disponibili</p>;77}7879export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {80 const { slug } = await params;8182 return (83 <>84 <ProductDetails slug={slug} />85 <Suspense fallback={<p>Controllo disponibilità...</p>}>86 <LiveInventory slug={slug} />87 </Suspense>88 </>89 );90}91```9293The page does not have to be all cached or all dynamic. The product sheet can be fast and reusable. Inventory can stay fresh. The user sees something right away, without waiting for the slowest part.9495## `use cache` is executable documentation9697The thing I like about `"use cache"` is that it forces you to make an intention explicit. When you read a function, you immediately understand that someone has decided: "this data can be reused".9899It's especially useful when you're not using `fetch`. Many apps read data from Prisma, Drizzle, internal SDKs, CMS clients or service functions. In those cases the old reasoning based only on `fetch` options wasn't enough.100101A rule of thumb:102103- cachea content relatively stable;104- use granular tags;105- leaves dynamic permissions, sessions, carts, notifications and transactional states;106- put slow parts inside `Suspense`;107- measure before saying "we improved performance".108109## Invalidate without throwing everything away110111The cache is only useful if you can update it accurately. Here `cacheTag`, `revalidateTag` and `updateTag` become important.112113Example:114115```typescript116'use server';117118import { updateTag } from 'next/cache';119120export async function updateProductName(productId: string, name: string) {121 await db.product.update({122 where: { id: productId },123 data: { name },124 });125126 updateTag(`product:${productId}`);127}128```129130The important detail is the tag. `product:${productId}` tells a precise boundary. `products` tells a huge bucket. At first the huge bucket is comfortable; after a few months it becomes the reason you invalidate half an app to change a title.131132## Stable Turbopack: the part you hear every day133134Next.js 16 brings Turbopack to the center for development and build. It's not the most poetic feature, but it's the one you feel while you work: server that starts earlier, faster refresh, builds that stop feeling like a forced coffee break.135136That said, I wouldn't migrate a codebase full of custom plugins with my eyes closed. I would check:137138- local build;139- non-standard import;140- MDX, SVG and CSS;141- Webpack plugins left;142- critical pages;143- differences in build times.144145For new projects, I would start from the default. For mature ones, I would do a measured migration.146147## React Compiler: remove noise, not thought148149React Compiler 1.0 is stable and Next.js 16 supports it with `reactCompiler`. The promise is to reduce a lot of manual memoization: less `memo`, less `useMemo`, less `useCallback` used "for safety".150151```typescript152// next.config.ts153import type { NextConfig } from 'next';154155const nextConfig: NextConfig = {156 reactCompiler: true,157};158159export default nextConfig;160```161162I wouldn't treat it like a magic dust. The compiler helps when the code follows React rules well. If components have strange side effects, hidden mutations or badly used hooks, that needs to be fixed first.163164The healthy way to try it:1651661. update `eslint-plugin-react-hooks`;1672. fix actual violations;1683. enable it on a controlled area;1694. measure build time and behavior;1705. remove manual memoization only when it is no longer needed.171172The goal is not to erase every `useMemo`. The goal is to stop writing preventive memoization because we are afraid of rendering.173174## `proxy.ts` and the network boundary175176The old `middleware.ts` becomes `proxy.ts`. It's a name change, but it makes sense: that file sits at the request boundary, it's not traditional backend-style generic middleware.177178```typescript179// proxy.ts180import { NextRequest, NextResponse } from 'next/server';181182export default function proxy(request: NextRequest) {183 const isLoggedIn = Boolean(request.cookies.get('session'));184185 if (!isLoggedIn && request.nextUrl.pathname.startsWith('/dashboard')) {186 return NextResponse.redirect(new URL('/login', request.url));187 }188189 return NextResponse.next();190}191```192193The rule here is simple: keep it small. Redirect, auth routing, headers, essential rewrites. If it starts to feel like a second backend, it's probably doing too much.194195## How I would really migrate196197I wouldn't turn on all the features at once. I would do this:1981991. I update Next, React and React DOM;2002. I launch the official codemod;2013. I fix breaking changes on `params`, `searchParams`, `cookies()`, `headers()` and `draftMode()`;2024. I migrate `middleware.ts` to `proxy.ts`;2035. I check builds and critical pages;2046. I enable Cache Components on a section where the cache currently creates friction;2057. I define conventions for tags and invalidation;2068. I try React Compiler separately;2079. comparison of metrics before and after.208209The good migration is not the one that uses all the new features. It's what makes the app's behavior more readable.210211## What changes in the way of thinking212213The most useful thing about Next.js 16 is that it forces you to name intentions better. A function is not just "get the product from the database". It's "get the product, I can cache it for hours, I invalidate it with this tag". A component is not just "render the page". It's "this is the fast shell, this piece is personal, this comes streaming."214215At first it seems like more work. Then it becomes a form of calm. Performance decisions are no longer hidden in a combination of defaults, heuristics and tribal memory. They're in the code.216217## Useful sources218219- [Next.js 16 release notes](https://nextjs.org/blog/next-16)220- [Cache Components - Next.js docs](https://nextjs.org/docs/app/getting-started/cache-components)221- [use cache - Next.js docs](https://nextjs.org/docs/app/api-reference/directives/use-cache)222- [React Compiler v1.0](https://react.dev/blog/2025/10/07/react-compiler-1)223
:Next.js 16, Cache Components and React Compiler: what really changeslines 1-223 (END) — press q to close