The App Router shipped as stable in Next.js 13, became the default in Next.js 14, and in Next.js 15 it received further refinements — partial pre-rendering stabilisation, improved caching defaults, and React 19 support. Vercel's position is clear: App Router is the future. But that doesn't mean the Pages Router is going away, or that migrating tomorrow is always the right call.
If you're starting a new Next.js project today, the decision is mostly straightforward. If you're working in an existing codebase, the calculus is genuinely more complex. This post maps out both architectures technically, identifies where each one wins, and covers the incremental migration path for teams that can't afford a big-bang rewrite.
What Changed Architecturally With App Router
The App Router is not just a different file-based routing convention — it's a different rendering model rooted in React Server Components (RSC). Understanding this distinction is the prerequisite for making an informed choice.
In the Pages Router, every component in your page tree is, by default, a Client Component. Data fetching happens through getServerSideProps, getStaticProps, or getStaticPaths — functions that run on the server before the page renders, passing data as props down to your component tree. The actual component code, however, ships to the browser and re-runs there for hydration.
In the App Router, components inside the app/ directory are Server Components by default. They run exclusively on the server, return HTML, and their source code never reaches the browser. You opt into client-side behaviour by adding "use client" at the top of a file. This inversion — server-first by default, client when needed — is the architectural shift everything else follows from.
Here's the same data-fetching pattern in both routers:
// Pages Router — pages/products/[id].tsx
export async function getServerSideProps({ params }) {
const product = await fetch(`/api/products/${params.id}`).then(r => r.json());
return { props: { product } };
}
export default function ProductPage({ product }) {
return <h1>{product.name}</h1>;
}
// App Router — app/products/[id]/page.tsx
async function getProduct(id: string) {
return fetch(`/api/products/${id}`).then(r => r.json());
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <h1>{product.name}</h1>;
}
The App Router version is simpler: the component itself is async, fetches data directly, and the result never re-runs on the client. No props plumbing, no separate data-fetching function. The simplicity scales well as component trees grow.
Where App Router Has Genuine Advantages
The App Router's wins are most pronounced in specific categories. Knowing them helps you assess whether they're relevant to your project.
Nested Layouts Without Boilerplate
The App Router's layout system lets you define a layout.tsx at any directory level. That layout persists across navigation within that route segment — it doesn't remount. A sidebar, a persistent nav, a dashboard shell: these render once and survive route transitions within their subtree. In the Pages Router, achieving the same required _app.tsx workarounds, persistent layout patterns, or state hoisting that felt more like fighting the framework than working with it.
Parallel Routes and Intercepting Routes
These are App Router-specific features with no direct Pages Router equivalent. Parallel routes (@slot conventions) let you render multiple independent route segments in the same layout simultaneously — the classic use case is a feed alongside a detail panel, both driven by the URL. Intercepting routes let a click on a link show a modal while keeping the underlying page in place, with the full page accessible if the URL is opened directly. Building either pattern in the Pages Router required custom state management and history manipulation.
Streaming and Suspense at the Layout Level
Server Components integrate with React's Suspense boundaries natively. You can stream parts of a page to the browser as they become available rather than waiting for the slowest data fetch to complete before sending anything. A product page with independently fetching sections (reviews, recommendations, inventory) can stream each section as its data arrives. The Pages Router's SSR model waits for getServerSideProps to resolve fully before sending any HTML.
Server Actions for Mutations
Server Actions (covered in detail in a separate post) let you define server-side mutation functions that Client Components can call directly — no API route required. Form submissions, cache invalidation, database writes: these can all live in server-only code without creating and maintaining separate route handlers. Pages Router projects need explicit API routes for every mutation.
Where Pages Router Is Still the Pragmatic Choice
The Pages Router retains real advantages in specific situations. These aren't niche edge cases — they affect large segments of the existing Next.js install base.
Migrating Large Existing Codebases
If you have a 200-page Pages Router application with thousands of components, migrating to the App Router involves re-evaluating every component's server/client split, updating data-fetching patterns, replacing layout boilerplate, and potentially restructuring third-party integrations. The opportunity cost — developer time that could ship features — is real. For these projects, continuing to build in the Pages Router while planning a phased migration is often more defensible than a full rewrite.
Third-Party Library Compatibility
Many React libraries assumed a client-first component model and haven't been updated to work cleanly with Server Components. UI libraries that rely on React Context at their top level, animation libraries like Framer Motion (which require "use client"), authentication libraries, and some analytics SDKs have varying levels of App Router compatibility. Before committing to App Router, audit your dependency list. If five core libraries require workarounds, the friction adds up quickly. The Pages Router works with all of them without modification.
Teams New to RSC Mental Model
The server/client component distinction introduces a mental model shift that takes time to internalise. Common mistakes — trying to use useState in a Server Component, passing non-serialisable props across the server/client boundary, importing a client library into a server component — produce errors that aren't always immediately clear to developers unfamiliar with the RSC model. For a team on a tight deadline delivering a project for the first time in Next.js, the Pages Router's simpler mental model (everything is a client component, use these three data-fetching functions) reduces risk.
getStaticProps-Heavy Sites
Static generation with getStaticProps and getStaticPaths is mature, well-understood, and produces excellent performance for content-heavy sites. The App Router's equivalent — making a server component that fetches data, combined with generateStaticParams — works, but the incremental static regeneration (ISR) behaviour differs in ways that can surprise teams coming from Pages Router. If your site relies heavily on ISR and your team knows the Pages Router ISR model well, changing it introduces unnecessary risk without a clear gain.
The Coexistence Migration Strategy
Next.js 15 supports running both routers simultaneously in a single project. Files in pages/ use the Pages Router. Files in app/ use the App Router. The two can link to each other using standard <Link> components or anchor tags, and they share the same Next.js server process.
The recommended migration sequence looks like this:
- Create an
app/directory alongside your existingpages/directory - Add a root
app/layout.tsxthat wraps shared UI (nav, footer) - Migrate low-traffic or newly created routes to the App Router first
- For each migrated route: identify which components need
"use client", update data fetching to async component patterns, removegetServerSideProps/getStaticProps - Leave high-traffic or complex routes in Pages Router until the new patterns are established
One important constraint: you cannot share a layout between both routers. The root _app.tsx applies only to pages/ routes; the root app/layout.tsx applies only to app/ routes. Any shared state (authenticated user context, theme) that both router sections need must live in providers that are duplicated or abstracted into a shared module.
The Metadata API Difference
SEO-conscious teams should pay attention to the metadata handling differences, as they affect how efficiently you can manage titles, descriptions, and Open Graph tags at scale.
In the Pages Router, metadata goes inside next/head tags within each page component. If you want a consistent title suffix or OG image across all pages in a section, you either repeat it in every file or build a wrapper component that manages it. Forgetting a page means it ships without proper metadata.
In the App Router, you export a static metadata object or a generateMetadata async function from any layout.tsx or page.tsx. Layout-level metadata cascades down to child routes, with page-level metadata merging on top. A single layout.tsx at app/blog/layout.tsx can set the default OG image, author, and Twitter card settings for every blog post in the directory. Individual posts only need to override the title and description.
// app/blog/layout.tsx — applies to all routes inside /blog
export const metadata = {
openGraph: {
images: [{ url: '/images/blog-default-og.png', width: 1200, height: 630 }],
type: 'article',
},
twitter: { card: 'summary_large_image' },
};
// app/blog/nextjs-router-guide/page.tsx — only overrides what's needed
export const metadata = {
title: 'Next.js 15 App Router vs Pages Router: Which to Choose in 2026',
description: 'App Router is stable and recommended for new projects...',
};
This inheritance model reduces metadata duplication significantly on large content sites.
Edge Deployment and Runtime Considerations
Both routers support edge runtime deployment, but the App Router makes it more granular. You can specify the runtime per-route with export const runtime = 'edge' in a page or layout file — some routes run on Node.js (for file system access, native modules), others run on the edge for lower latency. The Pages Router offers the same per-page runtime control, but without the layout-level granularity.
For teams deploying to Vercel's edge network, App Router's streaming model pairs naturally with edge infrastructure — streaming responses work well when the origin is geographically close to the CDN edge. For teams deploying to self-hosted Node.js servers or containers, the routing choice has less impact on deployment architecture.
Frequently Asked Questions
Can I use App Router and Pages Router in the same Next.js 15 project?
Yes. Next.js 15 fully supports running both routers side-by-side in a single project. Pages inside the app/ directory use the App Router; pages inside the pages/ directory use the Pages Router. Both routers share the same Next.js server, and you can link between them normally. The main limitation is that App Router-specific features — Server Components, nested layouts, streaming — are not available to Pages Router routes, and vice versa for getServerSideProps and getStaticProps. This hybrid mode is the recommended migration path: you migrate routes progressively rather than all at once.
Are there performance differences between App Router and Pages Router?
Yes, and the difference is most visible in JavaScript bundle size. App Router uses React Server Components by default, which means component code that runs only on the server never ships to the browser. A page that previously sent 300KB of JS for data-fetching logic might drop to 80KB in the App Router because that logic stays server-side. For content-heavy pages with minimal interactivity, the App Router consistently delivers smaller bundles and faster Time to Interactive. The Pages Router can match these numbers only if you aggressively code-split and avoid client-side data fetching — which requires deliberate effort rather than being the default behaviour.
Which router is better for SEO in Next.js?
The App Router has a more capable metadata system for SEO. You can export a generateMetadata function from any layout or page file, which lets you set dynamic titles, descriptions, Open Graph tags, and canonical URLs at the layout level — meaning a shared layout can handle metadata for all its child routes without duplication. The Pages Router uses the next/head component, which works fine but requires repeating head management in every page file. For streaming, the App Router can progressively send HTML as content is ready, which can improve Largest Contentful Paint. For sites where metadata complexity is the bottleneck, App Router is the stronger choice.