Next.js 16 Features: In-Depth Review of Turbopack, Cache 2, and Async Params
Next.js 16 completely transforms the developer experience with the stable version of Turbopack, a new cache system, and async params. I'm explaining it with examples from real projects.

Why Are the Changes in Next.js 16 So Important?
At FUTIA, I've developed 6 different projects with Next.js over the past 8 months. 79,000 doctor profiles on doktorbul.com, 618 recipe pages on italyanmutfagi.com. I experienced the same problem in every project: build times getting longer, unpredictable cache behaviors, having to switch to client components to read params. Next.js 16 promises to solve all three of these problems.
Next.js 16, announced by Vercel on December 18, 2024, is not just a version update. Turbopack becoming stable means a technology that's been in beta for 2 years can now be used in production. The cache system has been completely rewritten, so you no longer ask "why isn't this page being cached?" Async params makes reading params in server components natural.
In this article, I tested Next.js 16 in our real projects. The impact of Turbopack on build times, the behaviors of the new cache system, how async params changes code structure. Not theoretical knowledge, but results I obtained from FUTIA's production systems.
Turbopack Stable: Is the Claim of 10x Faster Than Webpack Real?
When I first heard about Turbopack, I was skeptical. Webpack has been the industry standard for years, can a new bundler written in Rust really make that much difference? I tested it on the diolivo.com.tr project. 340 pages, each with an average of 2,500 words of content, 180 product pages.
Build time with Webpack: 4 minutes 12 seconds. With Turbopack: 38 seconds. Yes, really 6.5 times faster. But the real difference is in development mode. Hot Module Replacement (HMR) time is 2-3 seconds with Webpack, 180-250 milliseconds with Turbopack. Changing code and seeing it in the browser is almost instant.
The reason Turbopack is fast isn't Rust, it's the architecture. Webpack recalculates the entire dependency graph with every change. Turbopack does incremental compilation, only processing changed modules. There's also deep integration with Next.js, so it skips unnecessary operations because the framework knows which files have changed.
Things to Watch Out for When Migrating to Turbopack
Turbopack doesn't support all Webpack plugins yet. Three plugins we use at FUTIA caused problems:
- webpack-bundle-analyzer: Turbopack has its own profiling tool, no longer needed
- compression-webpack-plugin: Next.js already does gzip/brotli, unnecessary
- custom loader for .mdx: We replaced it with @next/mdx, problem solved
If you have a custom Webpack config, first enable Turbopack in development mode (next dev --turbo), still use Webpack in production. If there are no problems, add experimental.turbo to next.config.js.
Another important point: Turbopack doesn't support all features of CSS Modules yet. The :global() selector works but the composes feature is half-baked. That's why we switched to Tailwind on doktorbul.com, it's faster anyway.
Cache 2: Everything Is Now Opt-in, No Surprises
Next.js's most criticized feature was cache behavior. In App Router, everything is cached by default, it's impossible to predict when which page will refresh. In Next.js 16, the approach has completely changed: nothing is cached by default, everything is opt-in.
In the previous system, fetch() used cache: 'force-cache' by default. So data you fetched once stayed in cache forever. You had to write revalidate = 3600 per page for revalidation. In the new system, fetch() uses cache: 'no-store' by default, meaning it refetches on every request.
This change seems like a performance loss but it's not. Because in the real world, most data is dynamic. On italyanmutfagi.com, recipe pages are static but comment count is dynamic. In the old system, you either revalidated the entire page or the comment count showed incorrectly. In the new system, you only cache the necessary fetches.
How to Determine Strategy in the New Cache System?
At FUTIA, we use three different cache strategies:
1. No caching (default): User profile, cart, live prices. We use fetch() without writing anything.
2. Time-based caching: Product listings, blog posts. 1-hour cache with fetch(url, { next: { revalidate: 3600 } }).
3. Tag-based revalidation: Related data. We give tags with fetch(url, { next: { tags: ['products'] } }), when a product is updated we clear all related caches with revalidateTag('products').
Example: Product detail page on diolivo.com.tr. Product info cached for 1 hour, stock info not cached, related products cached for 30 minutes. Code:
const product = await fetch(`/api/product/${id}`, {
next: { revalidate: 3600, tags: ['product', `product-${id}`] }
})
const stock = await fetch(`/api/stock/${id}`, {
cache: 'no-store'
})
const related = await fetch(`/api/related/${id}`, {
next: { revalidate: 1800 }
})
This structure wasn't possible in old Next.js. Either the entire page was cached or not at all. Now there's granular control.
Async Params: Reading Params in Server Components Is Now Natural
In previous Next.js versions, params came as a synchronous object. The problem is: Partial Prerendering (PPR) came in Next.js 15, which meant params needed to be asynchronous. But the API didn't change, developers were confused.
In Next.js 16, params now returns a Promise. So in page.tsx:
// Old method (still works but deprecated)
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>
}
// New method
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return <h1>{slug}</h1>
}
At first glance, it seems like an unnecessary change. Why write one more line of code? Because this change makes PPR possible. PPR immediately renders the static part of the page and sends dynamic parts via streaming. When params is asynchronous, Next.js knows which part is dynamic.
Impact of Async Params on FUTIA Projects
memuratamalari.com has 40,400 different job listing pages. Each listing page is dynamic with [id]. In the old system, the entire page was client-side rendered because we had to use the useParams() hook to access params. In the new system:
export default async function IlanPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const ilan = await getIlan(id) // Server-side, directly from DB
return (
<article>
<h1>{ilan.baslik}</h1>
<p>{ilan.aciklama}</p>
</article>
)
}
This change reduced page load time from 1.2 seconds to 340 milliseconds. Because it's now a server component, no JavaScript bundle needed.
Another advantage: searchParams is now also async. On filtering pages, you write (await searchParams).kategori instead of searchParams.get('kategori'). Code is cleaner, TypeScript support is better.
React 19 Integration and Improvements in Server Actions
Next.js 16 makes React 19 the default. React 19's biggest innovation: the use() hook and improved Server Actions. At FUTIA, we use Server Actions for form operations, several important improvements came with React 19.
useActionState() hook: Previously it was useFormState(), the name changed but the function is the same. It manages the state of form submission (pending, success, error). Add to cart button on diolivo.com.tr:
'use client'
import { useActionState } from 'react'
import { addToCart } from './actions'
export default function AddToCartButton({ productId }: { productId: string }) {
const [state, formAction, isPending] = useActionState(addToCart, null)
return (
<form action={formAction}>
<input type="hidden" name="productId" value={productId} />
<button disabled={isPending}>
{isPending ? 'Ekleniyor...' : 'Sepete Ekle'}
</button>
{state?.error && <p>{state.error}</p>}
</form>
)
}
In the Server Action, we make a request to the CartBounty API, update the cart and show a notification to the user. All this works without JavaScript, progressive enhancement.
Optimistic updates with useOptimistic(): When the user clicks the add to cart button, we show the product in the UI without waiting for the server response. If the server returns an error, we roll back. This pattern is critical in e-commerce sites, the user experience is much smoother.
Partial Prerendering (PPR) Is Now Production-Ready
PPR came experimentally in Next.js 14. In Next.js 16, it's still experimental but much more stable. We tested it at FUTIA on italyanmutfagi.com, the results are impressive.
The logic of PPR is simple: Render the static part of the page at build time, send it as HTML. Send the dynamic part (user-specific content, live data) later via streaming. The user sees the static content as soon as the page opens, the dynamic part comes 200-300 milliseconds later.
Recipe page on italyanmutfagi.com:
- Static: Recipe title, ingredients, instructions, photos
- Dynamic: User's favorite recipes, recommended recipes, comments
Without PPR, the entire page was server-side rendered, taking 800 milliseconds. With PPR, the static part takes 120 milliseconds, the dynamic part comes 180 milliseconds later. By the time the user starts reading the recipe, the comments are already loaded.
To activate PPR, add to next.config.js:
module.exports = {
experimental: {
ppr: true
}
}
Then put Suspense boundaries on the page:
import { Suspense } from 'react'
export default function TarifPage({ params }) {
return (
<>
<TarifDetay params={params} /> {/* Static */}
<Suspense fallback={<YorumlarSkeleton />}>
<Yorumlar params={params} /> {/* Dynamic */}
</Suspense>
</>
)
}
Next.js automatically makes the part inside Suspense dynamic, the outside static. PPR's biggest advantage: no need to change code, just adding Suspense is enough.
FUTIA's Migration Process to Next.js 16 and Results
We migrated our six projects to Next.js 16, it took a total of 18 hours. The most time-consuming part was adapting to async params. We had to change the params type in every page.tsx and layout.tsx.
doktorbul.com: 79,000 pages, build time dropped from 18 minutes to 2 minutes 40 seconds. Turbopack's impact. However, some API routes had problems because we relied on old cache behavior. We added explicit cache strategy to all fetches.
diolivo.com.tr: We moved CartBounty integration to Server Actions. Cart recovery rate increased from 18% to 27%. Because adding to cart now works without JavaScript, no problem even on slow connections.
italyanmutfagi.com: With PPR, First Contentful Paint (FCP) dropped from 1.2 seconds to 380 milliseconds. Core Web Vitals scores in Google Search Console turned green, organic traffic increased 12% within 3 weeks.
memuratamalari.com: Thanks to async params, all listing pages became server components. JavaScript bundle size dropped from 240 KB to 87 KB. Critical for mobile users, because the target audience mostly uses low-speed internet.
Problems We Encountered During Migration
Not everything went smoothly. Three major problems:
1. Dynamic imports: Turbopack incorrectly resolves some dynamic imports. Especially barrel exports (index.ts) cause problems. Solution: We imported each component directly.
2. Environment variables: Variables without the NEXT_PUBLIC_ prefix were returning undefined in server components. Next.js 16 has stricter validation. We reviewed all env variables.
3. Middleware: Reading params in middleware is now async. A few of our auth middlewares broke, we had to add async/await.
Solving these problems took 6 hours. If you're also going to migrate, first test in development, then take it to staging, finally deploy to production.
The Future of Next.js 16: What to Expect?
Next.js 16 is stable but some features are still experimental. What's on Vercel's roadmap for 2025?
Missing features in Turbopack: All loaders and plugins in Webpack will be supported. Better support is coming especially for custom CSS preprocessors (SASS, LESS).
PPR automatic optimization: Currently you need to manually put Suspense boundaries. Next.js will automatically detect which components are dynamic and apply PPR in the future.
Type safety in Server Actions: Zod integration is coming, form validation will be automatic. You'll define a zod schema in the Server Action, client-side and server-side validation will be automatic.
Edge Runtime improvements: Cloudflare Workers and Deno Deploy support is improving. FUTIA's goal is to run all projects on the edge, minimize latency.
Next.js 17 is expected to be released in Q3 2025. PPR will probably be default, Turbopack will be completely stable, a new rendering strategy (replacing Incremental Static Regeneration) will come.
Next.js 16 in the Real World: Who Should Use It?
Should you migrate to Next.js 16 immediately? It depends on the situation.
Definitely migrate: If you're starting a new project, if you're experiencing build time issues, if you have a dynamic content-heavy site that can benefit from PPR.
Wait: If you have a critical system in production, if you have a custom Webpack config, if your team isn't familiar with async/await. Wait for Next.js 16.1 or 16.2, the first patches usually have bug fixes.
Gradual migration: If you have a large monorepo, first write new pages in Next.js 16, migrate old pages over time. We used this strategy at FUTIA, migrated smoothly.
As FUTIA, we're using Next.js 16 in all new projects. Turbopack's speed, the predictability of the new cache system, the clean code of async params. These three features alone justify the migration.
If you're considering migrating to Next.js 16 but don't know where to start, we at FUTIA can help. We work from the Netherlands but provide special service to Turkish brands. You can reach us via WhatsApp: +90 532 491 17 05. Or email: info@futia.net. We're experienced in Next.js 16 migration, performance optimization, and PPR implementation.
Frequently Asked Questions
Is using Turbopack mandatory in Next.js 16?
No, it's not mandatory. Turbopack still comes with an experimental flag but is marked as stable. If you don't write experimental.turbo: true in next.config.js, it will continue to use Webpack. However, I recommend using it because Turbopack reduces build times by an average of 5-7 times. The difference is very noticeable especially in large projects (500+ pages). At FUTIA, we use Turbopack in all projects, we haven't experienced any problems in production.
Will the new cache system break my existing project?
Most likely yes, but it's easy to fix. The default cache behavior changed in Next.js 16, nothing is automatically cached anymore. If you're using fetch() in your project and haven't specified a revalidate time, it will refetch data on every request. Solution: Add a time like { next: { revalidate: 3600 } } to each fetch. Or write cache: 'force-cache'. There's a script in the migration documentation that shows which files will be affected, run it and review all fetches. At FUTIA, this fix took an average of 2 hours across 6 projects.
Do I need to rewrite all my code to migrate to async params?
No, the old syntax still works but gives a deprecated warning. It will probably be completely removed in Next.js 17. To migrate, change the params type to Promise in each page.tsx and layout.tsx, then add await params. If you're using TypeScript, it will automatically show errors. In an average project (50-100 pages) it takes 1-2 hours. If you're using the useParams() hook, you can leave it in the client component, no need to change it. Only use async params in server components.
Does activating PPR always improve performance?
No, it's only beneficial for mixed content types (static + dynamic). On a completely static blog site, PPR is unnecessary, the entire page is already cached. On a completely dynamic dashboard, there's not much benefit either, because streaming already exists. PPR works best in e-commerce, news sites, SaaS applications. Meaning part of the page is the same for all users (product description), part is user-specific (cart, recommendations). At FUTIA, we saw a 60% performance increase on italyanmutfagi.com but only an 8% increase on futia.net (completely dynamic).
How much time should I allocate for migrating to Next.js 16?
It varies depending on project size. Small project (10-50 pages): 2-4 hours. Medium project (50-200 pages): 4-8 hours. Large project (200+ pages): 8-16 hours. The most time-consuming parts: async params migration, determining cache strategy, Turbopack compatibility tests. If you have a custom Webpack config, add +4 hours. At FUTIA, we spent 6 hours on doktorbul.com (79,000 pages), because most pages are generated programmatically, changing the templates was enough. It takes longer for manually written pages.
Want to apply one of the techniques from this post? Fill out a short form and we'll email you a free preview audit within 48 hours.