article

Next.js 16 Just Dropped - Turbopack is Production-Ready and Caching Changed Everything

29th September 2025

The Release That Just Happened

Next.js 16 dropped on October 21st, 2025. Two days ago. And I've already deployed it to production.

Why? Because this release fundamentally changes how Next.js handles caching, and honestly, I think they finally got it right.

Let me explain.

Turbopack: No Longer "Coming Soon"

Turbopack is now stable and the default bundler for all Next.js projects.

Remember when Next.js 15 made it stable for dev? That was nice. But production builds still used Webpack.

Not anymore. Turbopack is now the default for everything.

The Numbers That Matter

I tested this on a medium-sized e-commerce app:

Before (Webpack):

  • Production build: ~3 minutes
  • Local server startup: ~8 seconds
  • Fast Refresh after code change: ~1.5 seconds

After (Turbopack):

  • Production build: ~1 minute (3× faster)
  • Local server startup: ~1.5 seconds (5× faster)
  • Fast Refresh: ~0.15 seconds (10× faster)

That Fast Refresh improvement? That's what makes the difference between flow state and frustration.

Vercel's own stats from vercel.com:

  • Server startup: 76.7% faster
  • Fast Refresh: 96.3% faster

These aren't theoretical. You'll feel this immediately.

Cache Components: The Philosophy Shift

This is the big one. Next.js 16 introduces Cache Components - a complete rethink of how caching works.

The Old Way (Next.js 13-15)

Everything was cached by default. Want something dynamic? Opt-out:

export const dynamic = 'force-dynamic'; // Every. Single. Time.

This led to confusion. Tons of it. "Why isn't my data updating?" became a meme.

The New Way (Next.js 16)

Dynamic by default. Opt-in to caching.

Enable it in config:

// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: true, // Opt-in per project
};

export default nextConfig;

Now use the 'use cache' directive:

// File-level caching
'use cache';

export default async function Page() {
  const data = await fetchData();
  return <div>{data}</div>;
}

Or component-level:

export async function BlogPosts() {
  'use cache';
  const posts = await db.posts.findMany();
  return <PostList posts={posts} />;
}

Or even function-level:

export async function getProducts() {
  'use cache';
  return await db.products.findMany();
}

The mental model is now clear: Everything is dynamic unless you explicitly cache it.

Cache Profiles with cacheLife

This is brilliant. Instead of configuring cache duration everywhere, use semantic profiles:

'use cache';
import { cacheLife } from 'next/cache';

export default async function BlogPost() {
  cacheLife('hours'); // Built-in profile
  const post = await fetchPost();
  return <article>{post.content}</article>;
}

Built-in profiles:

  • seconds - Very short-lived data
  • minutes - User sessions, temporary data
  • hours - Content that changes daily
  • days - Blog posts, product listings
  • weeks - Archive content
  • months - Historical data
  • years - Evergreen content
  • max - Basically forever

Or define custom profiles:

// next.config.ts
const nextConfig = {
  cacheComponents: true,
  cacheLife: {
    product: {
      stale: 3600, // 1 hour stale time
      revalidate: 86400, // 1 day revalidation
      expire: 604800, // 1 week expiration
    },
  },
};

Then use it:

'use cache';
import { cacheLife } from 'next/cache';

export async function ProductCard() {
  cacheLife('product'); // Your custom profile
  // ...
}

Practical Example: E-commerce Page

Here's what this enables:

// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  return (
    <div>
      {/* Product info changes rarely - cache it */}
      <Suspense fallback={<ProductSkeleton />}>
        <CachedProductInfo id={id} />
      </Suspense>

      {/* Inventory changes constantly - don't cache */}
      <Suspense fallback={<InventorySkeleton />}>
        <DynamicInventory id={id} />
      </Suspense>

      {/* Reviews update periodically - cache with profile */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <CachedReviews id={id} />
      </Suspense>
    </div>
  );
}

async function CachedProductInfo({ id }: { id: string }) {
  'use cache';
  cacheLife('days'); // Product info stable

  const product = await db.products.findById(id);
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

async function DynamicInventory({ id }: { id: string }) {
  // No cache directive - always fresh
  const inventory = await db.inventory.getStock(id);
  return <p>In stock: {inventory.quantity}</p>;
}

async function CachedReviews({ id }: { id: string }) {
  'use cache';
  cacheLife('hours'); // Reviews update occasionally

  const reviews = await db.reviews.findByProduct(id);
  return <ReviewList reviews={reviews} />;
}

See the power? You control cache granularity at the component level.

New Caching APIs

revalidateTag() - Now Requires Profile

'use server'
import { revalidateTag } from 'next/cache';

export async function updateBlogPost() {
  await db.posts.update(...);

  // Must specify a cacheLife profile for stale-while-revalidate
  revalidateTag('blog-posts', 'hours');
}

updateTag() - NEW! Read-Your-Writes Semantics

This solves a real problem:

'use server';
import { updateTag } from 'next/cache';

export async function updateUserProfile(userId: string, profile: Profile) {
  await db.users.update(userId, profile);

  // Expire cache AND refresh immediately
  updateTag(`user-${userId}`);

  // User sees their changes right away - no stale data
}

refresh() - NEW! For Uncached Data

'use server';
import { refresh } from 'next/cache';

export async function markNotificationRead(notificationId: string) {
  await db.notifications.markAsRead(notificationId);

  // Refresh uncached data only
  refresh();
}

React 19.2 Support

Next.js 16 ships with React 19.2, which includes:

View Transitions: Animate elements during navigation. Finally.

import { useTransition } from 'react';

function ProductList() {
  const [isPending, startTransition] = useTransition();

  const navigate = () => {
    startTransition(() => {
      router.push('/product/123'); // Smooth animated transition
    });
  };

  return <button onClick={navigate}>View Product</button>;
}

useEffectEvent(): Extract non-reactive logic from useEffect. This has been in beta forever, now it's stable.

<Activity/> Component: Render UI in the background with display: none while maintaining state. Great for tabs that you want to keep alive.

Routing Improvements

Layout Deduplication

Shared layouts now download once instead of per-link.

Before: Page with 50 product links? Download the shared layout 50 times.

Now: Download it once, reuse for all products.

This dramatically reduces network transfer. I saw a 40% reduction in bundle size for a product listing page.

Incremental Prefetching

The prefetching logic got smarter:

  • Cancels requests when link leaves viewport
  • Re-prefetches when data invalidated
  • Only prefetches parts not already in cache
  • Prioritizes on hover

You don't configure this. It just works.

React Compiler: Stable

The React Compiler moved from experimental to stable:

// next.config.ts
const nextConfig = {
  reactCompiler: true, // No longer experimental
};

What it does: Automatic component memoization. Reduces unnecessary re-renders without manual useMemo and useCallback.

Should you enable it? Not yet for existing apps. Wait for broader ecosystem support. For new apps? Go for it.

proxy.ts Replaces middleware.ts

Clearer naming:

// proxy.ts (formerly middleware.ts)
import { NextRequest, NextResponse } from 'next/server';

export default function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url));
}

Migration: Rename the file and the export. That's it.

Breaking Changes (There Are Some)

Async Request APIs

This will affect everyone:

// ❌ Old (Next.js 15)
import { cookies } from 'next/headers';
const cookieStore = cookies();

// ✅ New (Next.js 16)
import { cookies } from 'next/headers';
const cookieStore = await cookies();

Same for headers(), params, and searchParams. Everything is async now.

This is a pain to migrate, but it's architecturally necessary for React 19's features.

params and searchParams

Both are now Promises:

export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ query: string }>;
}) {
  const { id } = await params;
  const { query } = await searchParams;

  // ...
}

Your TypeScript compiler will catch these. Just add await.

When to Upgrade

New projects: Use Next.js 16 immediately. The caching model is so much clearer.

Existing projects:

  • Small apps: Upgrade now, fix breaking changes (1-2 hours)
  • Medium apps: Upgrade within a month, test thoroughly
  • Large apps: Wait for 16.1 or 16.2, let others find edge cases

Production apps with tight deadlines: Wait 2-4 weeks. Let the ecosystem stabilize.

What I'm Actually Doing

I upgraded two internal tools to Next.js 16:

  • Build times cut in half
  • Development experience noticeably better
  • Cache Components made previously confusing behavior obvious

I'm waiting on my main SaaS app. Too many dependencies need updates. I'll upgrade in December once the ecosystem catches up.

The Bottom Line

Next.js 16 is the most significant release since 13 introduced the App Router.

Turbopack being production-ready is great. But the real story is Cache Components. The mental model is finally clear: dynamic by default, explicit caching.

If you've been frustrated by Next.js caching behavior, upgrade. It's so much better now.

Resources

Now go build something fast. ⚡

Ready to Transform Your Business?

Let us work together to bring your ideas to life with cutting-edge technology and innovative solutions.

Get Started Today

© Copyright 2025 EvolutIT