🎉 hey, I shipped skillcraft.ai It's like Reddit, but for tech courses

As a developer myself, I know how important it is to keep learning, which is why I created this community.

Published
13 min read

What's new in Next.js 16

Async params, Turbopack by default, and the cleanup of experimental features

Next.js 16 is here, and it’s the kind of release where the team finally stabilizes everything they’ve been testing for the past year. Experimental features are graduating to stable, deprecated APIs are getting removed, and the whole framework is getting faster.

If you’re planning to upgrade, here’s what changed and why it matters.

Route parameters are now asynchronous

The biggest change you’ll notice is that route parameters are now async. Every component that used to just grab params or searchParams now needs to await them. This hits pages, layouts, route handlers, and even those specialized image generation functions (opengraph-image, twitter-image, icon, apple-icon).

The pattern looks simple enough at first:

But it goes deeper than just pages. Layouts need the same treatment. In route handlers, segmentData.params is now a promise. Even generateMetadata functions need you to await both params and searchParams before you can use them.

Image generation functions get it too. When you’re using generateImageMetadata, both params and id are promises:

Why the change? It’s about streaming and concurrent rendering. When params are synchronous, Next.js has to block and wait for everything to resolve before it can start rendering. Making them async lets the framework start streaming your page shell while params are still being figured out in the background.

This matters more when params come from different places. Sure, a slug gets extracted from the URL. But sometimes route parameters need database validation, edge config resolution, or network requests. The async pattern gives Next.js room to optimize all of that without blocking your entire render pipeline.

If you’re using TypeScript, get ready for some type updates. Instead of Params = { slug: string }, you’re writing Params = Promise<{ slug: string }>. That propagates everywhere params are used, which in a big app can be… a lot of files.

For client components that need route parameters, React’s use hook lets you unwrap the promise:

The automated codemod handles a lot of this, but it’s not perfect. Complex destructuring, params passed through multiple functions, or params used in helper functions might need manual fixes. When the codemod can’t figure something out, it leaves @next-codemod-error comments showing you where to look.

Turbopack becomes the default bundler

Turbopack’s been in the works for years. Vercel rebuilt their bundler from scratch in Rust, and in Next.js 16, it’s finally the default. No more flags, no experimental config. Run next dev or next build, and you’re using Turbopack.

The transition is smooth. Drop the --turbopack flag from your package.json scripts. If you had "dev": "next dev --turbopack", just make it "dev": "next dev". Same for build commands.

The performance difference is real, especially on big apps. Cold starts are faster when you spin up dev. Hot module replacement feels instant. Production builds speed up too, and the gains scale with how many modules you have. If you’re working on something with thousands of modules, you’ll notice.

Config-wise, Turbopack moved out of the experimental section:

There’s a new filesystem caching feature worth knowing about. Enable experimental.turbopackFileSystemCacheForDev or experimental.turbopackFileSystemCacheForBuild, and Turbopack saves compilation artifacts in .next between runs. Restart your dev server or run another build, and it’s way faster. In CI/CD, if you cache the .next directory, your builds speed up significantly.

Tracing got better too. Run npx next internal trace .next/dev/trace-turbopack and you get detailed traces showing where compilation time goes. Super helpful when you’re debugging slow builds.

If you need Webpack for something specific, there’s an opt-out. Add --webpack to your build command: "build": "next build --webpack". Dev still uses Turbopack, but you’ve got an escape route.

Legacy Webpack patterns might need tweaking. Take Sass imports with the tilde prefix (~). That was a Webpack thing. Turbopack doesn’t support it out of the box, but you can set up a resolve alias:

Same deal if client code accidentally imports Node modules like fs. You can set up fallbacks to avoid build errors. Better to fix the imports, but resolveAlias gets you through the migration.

React Compiler support

With React 19, the React Compiler went stable. Next.js 16 supports it as a production-ready feature.

The compiler automatically memoizes your components to cut down on unnecessary re-renders. Less time wrapping everything in useCallback and useMemo.

Enable it like this:

It’s stable but opt-in. You control when to turn it on. How much it helps depends on your app. Components with gnarly re-render patterns will see the biggest wins.

Improved caching APIs

Next.js caching has always been… complicated. Too much magic, too much guessing about what’s cached and when it expires. Version 16 fixes this by stabilizing caching APIs that put you in control.

These APIs lost their unstable_ prefix and became production-ready. The shift is from automatic, implicit caching to explicit, developer-controlled caching.

cacheLife lets you set exactly how long cached data lives. No more guessing from config files. You declare it in code. User profile data caches for hours, stock prices for seconds. You decide.

cacheTag gives you a tagging system. Fetch some data, tag it. Later, when that data changes, invalidate by tag instead of nuking everything. User updates their profile? Invalidate the user-123 tag, not all user data.

updateTag solves the “read-your-writes” problem. User submits a form, updates their profile, they want to see the change immediately. Without updateTag, they might get stale cache. updateTag expires the tag and fetches fresh data in the same request. User sees their own changes instantly:

The difference between updateTag and revalidateTag? revalidateTag marks data as stale. Users see old data while new data loads in the background. Good for articles or content where staleness is fine. updateTag blocks until fresh data arrives. Immediate visibility.

refresh triggers a client-side router refresh from server actions. Mutate data on the server, tell the client to refresh the affected UI:

Imports are cleaner now:

Also, experimental.dynamicIO got renamed to cacheComponents and went stable. Related to Partial Pre-Rendering (PPR):

unstable_cache still exists (still has the unstable prefix) for wrapping async functions with caching. Configure it with tags and revalidation:

All this lines up with Next.js’s push toward React Server Components and streaming. When you declare cache behavior explicitly, the framework knows when to fetch, stream, or serve from cache.

Middleware renamed to Proxy

Middleware is now called proxy. Why? “Middleware” means different things in different frameworks. “Proxy” is clearer. It sits between the request and your app, proxying and modifying requests before they hit your routes.

Migration is simple. Rename the file:

Update your exports:

The bigger change: proxy functions only run on Node.js runtime now. Edge runtime support is gone. If you relied on Edge middleware for low-latency request interception, this breaks things.

Why drop Edge? Maintaining two runtimes for the same feature was complex. Edge has constraints Node.js doesn’t. Different APIs, package compatibility issues. Standardizing on Node.js simplifies everything and reduces weird edge cases.

If you were using Edge middleware for geolocation routing, A/B testing, or auth checks near users, you’ve got options. Adapt your proxy logic for Node.js. If you really need Edge proximity for latency, move that logic outside Next.js. Vercel Edge Functions, Cloudflare Workers, whatever your host offers.

One pattern worth considering: use proxy functions purely for routing and rewriting. Keep auth and dynamic logic in server components or route handlers. You get React Server Components’ streaming while keeping security boundaries intact.

There’s a new config option, skipProxyUrlNormalize, that controls URL normalization before it hits your proxy. Matters if you need to preserve specific URL formatting or query param order.

Proxy functions can’t return response bodies anymore. Previously, you could return full responses from middleware, JSON or HTML. Not in Next.js 16. Proxy is strictly for request modification, rewriting, and redirection. Custom responses belong in route handlers or API routes.

Example: auth middleware that returned 401 JSON now redirects to login:

Better separation of concerns. Request interception stays in proxy. Response generation stays in your routes.

AMP support removed

AMP is gone. No more useAmp hook, no amp page config, no hybrid AMP/HTML pages.

Why? AMP adoption declined as browsers improved and frameworks like Next.js got faster. Modern Next.js already delivers the performance benefits AMP promised. Removing it simplifies the framework.

If you’re using AMP, you’ll need alternatives before upgrading. The good news: Next.js’s built-in optimizations probably already match or beat what AMP gave you.

Image optimization changes

Image handling in Next.js 16 got tighter security and better performance. Changes to how next/image processes and caches images, all based on real-world data and security issues.

Local images with query strings now need explicit config. Before, you could do <Image src="/assets/photo?v=1" /> without setup. That created an attack vector for filesystem enumeration. Now you declare it:

Whitelist-based security instead of permissive. If your image versioning uses query params, declare the patterns upfront.

Default minimum cache TTL jumped from 60 seconds to 4 hours (14400 seconds). Cost optimization. Image revalidation is expensive—bandwidth, processing, CDN costs. Most images don’t change fast enough to justify revalidation every minute. When Next.js processes an external image without explicit Cache-Control headers, it assumes longer cache lifetime.

Need shorter cache? Set it explicitly:

Image quality defaults changed to fix srcset bloat. Before: multiple quality variants for responsive images. Now: single quality level ([75]) by default. Smaller srcset attributes, fewer variants to process and cache. For most apps, one quality level works. Need multiple (premium vs. bandwidth-constrained)? Declare them:

16px got dropped from the default imageSizes array. Why? Usage analysis showed 16px images are almost all favicons, which don’t go through next/image anyway. Fewer variants for responsive images. Need 16px? Add it back:

Remote images got security hardening. images.domains is deprecated. Use images.remotePatterns with explicit protocol:

Prevents accidental HTTP loading. Makes security explicit. Restrict further by pathname and port if needed.

images.maximumRedirects limits HTTP redirects when fetching images. Default changed from unlimited to 3. Prevents DoS where an image URL redirects forever. Disable redirects (maximumRedirects: 0) or increase for edge cases.

For local IP development, images.dangerouslyAllowLocalIP is your escape hatch. Next.js blocks image optimization for local IPs by default. Set to true to allow it. The “dangerously” prefix means use this only in private networks.

Runtime configuration replaced with environment variables

serverRuntimeConfig and publicRuntimeConfig are gone. Use environment variables instead. Simpler, works everywhere.

Migration:

Client-side vars need NEXT_PUBLIC_ prefix. Server-side vars don’t. That’s it.

ESLint integration changes

next lint command removed. Use ESLint directly.

Codemod for migration:

Updates your project to use ESLint directly. The eslint config option in next.config.js is gone too.

Migration strategy

Upgrading to Next.js 16 needs care. The breaking changes are big enough that rushing will give you runtime errors or bugs that only show up in production.

Start with the automated codemod:

This handles the mechanical stuff automatically. Updates package.json to Next.js 16 and React 19. Converts synchronous params and searchParams to async, adding await and marking functions async. Renames experimental.dynamicIO to cacheComponents. Removes unstable_ prefix from cache imports.

The codemod isn’t perfect. It runs on static analysis and can’t always figure out the right transformation. When it’s stuck, it leaves @next-codemod-error comments showing what needs manual review.

Common manual fixes: params passed through helper functions, complex destructuring the codemod can’t parse, components exported/imported across files where types aren’t available, params stored in variables then passed to other functions.

After the codemod, test systematically. Route handlers are high priority—segmentData.params affects every route handler with dynamic segments. Page components with dynamic routes or search params need testing. Layout components that access params need checking. Metadata generation functions now await both params and searchParams.

Images need attention. Using query strings on local images? Add localPatterns config. Review external image domains. Migrate from domains to remotePatterns. Check image caching config works with the new 4-hour default TTL.

Middleware migration is more than renaming. Verify logic works on Node.js runtime if you used Edge. Middleware that returned response bodies needs refactoring to redirects/rewrites. Test auth flows carefully—response bodies to redirects changes UX.

Environment variables are critical if you used serverRuntimeConfig or publicRuntimeConfig. Server-side values in env vars without NEXT_PUBLIC_ prefix. Client-side values prefixed with NEXT_PUBLIC_ and accessible in client components.

TypeScript projects have extensive type changes. Update everywhere you referenced params or searchParams. { slug: string } becomes Promise<{ slug: string }> everywhere. Run tsc --noEmit to catch type errors before runtime.

Dev vs. production behavior can differ. Always test production builds locally before deploying. Run npm run build then npm start. Check caching behavior—it differs between dev and production.

Parallel routes got a breaking change that’s easy to miss: empty slots now require default.js. Using parallel routes with potentially empty slots? Create default.js files that return null or call notFound():

Scroll behavior changed subtly. Next.js 16 doesn’t override CSS scroll-behavior: smooth during navigation anymore. If you relied on the old behavior (Next.js temporarily setting scroll to auto), add data-scroll-behavior="smooth" to your root HTML:

CI/CD pipelines might need tweaking. Caching .next between builds? Directory structure changed. New .next/dev directory enables concurrent dev and build. Update cache config to include it. Using Turbopack filesystem caching in CI? Cache the entire .next directory.

Build adapters hit alpha with an experimental API. Working on custom deployment targets or need to hook into the build process? experimental.adapterPath points to a custom adapter module. Advanced feature for hosting providers and framework authors.

Error monitoring might surface new patterns from async params. Make sure error tracking captures full async stack traces. Some error boundaries might need adjusting if they caught errors from synchronous param access.

For complete details, see the official Next.js 16 announcement.


Found this article helpful? You might enjoy my free newsletter. I share dev tips and insights to help you grow your coding skills and advance your tech career.


Check out these related articles that might be useful for you. They cover similar topics and provide additional insights.

Webdev
4 min read

Optimize Your Astro Site's <head> with astro-capo

Automatically improve your Astro site's performance using astro-capo

Oct 19, 2024
Read article
Webdev
4 min read

The What, Why, and How of Using a Skeleton Loading Screen

Skeleton loading screens enhance user experience and make your app feel faster

Nov 12, 2020
Read article
Webdev
7 min read

Tips for Reducing Cyclomatic Complexity

Cyclomatic complexity is like counting how many ways a car can go. More options make it harder to drive because you have to make more decisions, which can lead to confusion.

Sep 10, 2024
Read article
Webdev
3 min read

Preloading Responsive Images

How to properly preload responsive images to improve initial page load

Nov 28, 2024
Read article
Webdev
12 min read

Frontend Security Checklist

Tips for Keeping All Frontend Applications Secure

Jul 30, 2024
Read article
Webdev
3 min read

CSS :has() - The Parent Selector We've Always Wanted

Transform your CSS with :has(), the game-changing selector that finally lets us style elements based on their children.

Dec 4, 2024
Read article
Webdev
3 min read

CVE-2025-29927 - Next.js Middleware Bypass Explained In Simple Terms

The vulnerability skips Next.js middleware security checks by adding a single HTTP header

Apr 6, 2025
Read article
Webdev
4 min read

HTTP CONNECT: Building Secure Tunnels Through Proxies

Understand how HTTP CONNECT enables HTTPS traffic through proxies

Nov 28, 2024
Read article
Webdev
36 min read

IndexNow: Get your content indexed instantly by AI search engines and traditional search

Stop waiting weeks for crawlers. Learn how to notify Bing, DuckDuckGo, ChatGPT, and Perplexity instantly when you publish new content using the free IndexNow protocol

Oct 27, 2025
Read article

This article was originally published on https://www.trevorlasn.com/blog/whats-new-in-nextjs-16. It was written by a human and polished using grammar tools for clarity.