Next.js 15 App Router: What Changed and Why It Matters

After migrating two production apps from Next.js 14 to 15, I can say this: the upgrade is smoother than 13-to-14, but the behavioral changes will catch you off guard if you don’t read the release notes carefully.
Caching Is No Longer Aggressive by Default
This is the biggest shift. In Next.js 14, fetch() requests were cached by default — you had to opt out with cache: 'no-store'. Next.js 15 flips this. Fetches are now uncached by default, which means your pages that relied on implicit caching might suddenly feel slower.
To restore the old behavior on a per-request basis:
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // cache for 1 hour
});
Or you can set a route segment config to cache everything in a layout or page:
export const fetchCache = 'default-cache';
I actually prefer the new default. Explicit caching forces you to think about staleness, which is how it should be. The old “everything cached” approach led to subtle bugs where content updates didn’t appear until a redeploy.
Async Request APIs
Headers, cookies, params, and searchParams are now asynchronous. If you had code like this:
// Next.js 14
export default function Page({ params }: { params: { slug: string } }) {
return <Article slug={params.slug} />;
}
You now need:
// Next.js 15
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return <Article slug={slug} />;
}
Same goes for headers() and cookies() — they return promises now. The codemod (npx @next/codemod@canary upgrade) handles most of these, but I found it missed a few edge cases in deeply nested components.
Partial Prerendering (PPR)
PPR is the feature I’m most excited about. It lets you serve a static shell instantly while streaming dynamic content. Think of it as the best of both SSG and SSR combined.
Enable it in your config:
// next.config.ts
const nextConfig = {
experimental: {
ppr: 'incremental',
},
};
Then mark specific routes:
export const experimental_ppr = true;
Your static content (nav, footer, layout) renders immediately from the edge cache, while dynamic sections (user data, personalized content) stream in via Suspense boundaries. The user sees something instantly instead of staring at a blank page.
Server Actions Got More Reliable
Server Actions in 14 felt like a beta feature. In 15, they handle errors more gracefully, the TypeScript types are tighter, and redirects from within actions work properly. One pattern I’ve adopted:
'use server';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const response = await fetch(`${process.env.WP_API_URL}/wp/v2/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.WP_AUTH_TOKEN}`,
},
body: JSON.stringify({ title, content, status: 'publish' }),
});
if (!response.ok) {
throw new Error('Failed to create post');
}
revalidatePath('/blog');
redirect('/blog');
}
The Turbopack Situation
Turbopack for dev is now stable and noticeably faster — cold starts on my project dropped from ~4 seconds to under 1 second. Production builds still use Webpack, but Turbopack for next dev is solid enough to use daily. Run it with next dev --turbopack.
Should You Upgrade?
If you’re on 14 with the App Router, yes. The caching change alone prevents a whole class of “why isn’t my content updating” bugs. Run the codemod first, fix the async API changes manually where needed, and test your data fetching thoroughly.
If you’re still on Pages Router, this release doesn’t add much urgency to migrate. But the App Router is clearly where all investment is going.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


