In 2022, a developer reproduced the official Next.js App Router example app under the official Pages Router example app, using the same MUI components, and filed it as a GitHub issue: the Pages Router version shipped 141 KB of First Load JS; the App Router version shipped 200 KB — a 42 percent increase, in the framework's own reference implementation (https://github.com/mui/material-ui/issues/39527). That result runs directly against the pitch every App Router explainer opens with — smaller bundles, because Server Components don't ship JavaScript to the client. Both things are true. The gap between them is the actual story: whether the App Router reduces what a browser downloads depends entirely on how much of the component tree stays server-rendered, and a benchmark number without that context is close to meaningless.
The Bundle-Size Number Depends on Where the 'use client' Boundary Actually Sits
The MUI case is instructive because it isn't a misconfiguration — it's what happens by default when a component library built around client-side context providers (theming, a11y state, portals) gets dropped into the App Router without restructuring. If the root layout needs 'use client' to make that provider work, everything beneath it in the tree hydrates on the client anyway, and the App Router version now carries the RSC runtime overhead on top of the same client bundle the Pages Router was already shipping. The framework didn't fail here; the migration just moved the boundary in the wrong place.
Contrast that with a case where the boundary was deliberately drawn: one engineering write-up profiling a SaaS dashboard route reported First Load JS dropping from roughly 280 KB to about 165 KB — a 41 percent reduction — after moving a data table to a Server Component and keeping only the filter controls as 'use client' (https://www.kunalganglani.com/blog/nextjs-app-router-vs-pages-router). Same framework, same version era, opposite result, because the actual variable being measured wasn't "App Router versus Pages Router." It was "how much of this page's interactivity genuinely needs to run in the browser," which the App Router merely makes explicit and the Pages Router left implicit by hydrating the whole tree regardless.
Same claim, two outcomes: the App Router's official example shipped 42 percent more JavaScript than its Pages Router counterpart; a deliberately restructured dashboard route shipped 41 percent less — the router changed nothing on its own in either case.
Streaming Is the Part of the Pitch That Holds Up Consistently
Bundle size is architecture-dependent. Streaming is closer to a structural guarantee, because it comes from how the App Router's rendering model handles slow data by default rather than from how carefully a team drew component boundaries. Under the Pages Router, getServerSideProps blocks the entire response until every data dependency for a page resolves — a page with three sequential API calls waits for all three before sending a byte of HTML. The App Router can wrap slow sections in <Suspense> and stream the page shell immediately, filling in the data-dependent regions as they resolve.
Pages Router request
→ getServerSideProps (blocks)
→ fetch A → fetch B → fetch C
→ full HTML sent only after all three resolve
App Router request
→ page shell streamed immediately
→ <Suspense fallback={<Skeleton/>}>
→ slow fetch resolves independently, streamed in when ready
Reports from teams migrating data-heavy dashboard routes describe the user-visible effect as a loading skeleton appearing within 100 to 200 milliseconds instead of a blank screen for 800 milliseconds or more while sequential fetches complete — and separately, one engineer's write-up citing community benchmarking on Next.js's GitHub discussions points to Total Blocking Time reductions in the 20 to 40 percent range on complex dashboard routes migrated to Server Components (https://www.kunalganglani.com/blog/nextjs-app-router-vs-pages-router). That figure is self-reported and route-specific rather than a controlled benchmark, but it's directionally consistent with what the streaming model predicts: the win shows up specifically on routes with slow, independent data dependencies, not on simple static pages where there's nothing to stream around.
TTFB Comparisons Usually Aren't Measuring What They Claim To
A recurring claim in App Router marketing is faster Time to First Byte. For statically generated pages served from a CDN edge node, TTFB is close to identical between routers, because both are serving a pre-built HTML file — the router that generated it at build time is irrelevant to how fast the edge node returns it at request time. The gap opens up only on dynamically rendered routes, where the App Router's ability to stream a shell before all data resolves genuinely front-loads perceived response time even though the full response isn't faster.
One performance consultancy's benchmarking of e-commerce product pages reported p75 Largest Contentful Paint improvements of 600 to 900 milliseconds after moving from Pages Router with hand-built incremental static regeneration to App Router streaming SSR with per-product on-demand revalidation (https://webvitals.tools/blog/nextjs-vs-remix-performance/). That's a real number, but it's measuring a migration that changed two things at once — the router and the caching strategy — and the same source noted that on sites where the LCP bottleneck was actually image and font delivery rather than server response time, the router change alone produced far smaller gains. Any TTFB or LCP figure attached to a router comparison is worth checking against what else moved in the same migration before crediting the router for it.
The Number Every Benchmark Chart Leaves Out: Attack Surface
None of the comparisons above touch the one differentiator between the two routers that isn't a matter of degree. In December 2025, React and Next.js disclosed CVE-2025-55182 and its downstream Next.js tracking identifier, CVE-2025-66478 — a CVSS 10.0 unauthenticated remote code execution vulnerability in the RSC "Flight" protocol, the serialization format Server Components use to stream data between server and client (https://nextjs.org/blog/CVE-2025-66478). A malformed payload sent to any Server Function endpoint could trigger arbitrary server-side code execution without credentials, in the framework's default configuration. The fix required a framework-level upgrade; there was no configuration workaround.
It was not an isolated patch. Within the same two months, three related disclosures followed as researchers examined the original fix:
CVESeverityDisclosedIssueCVE-2025-55182 / CVE-2025-66478CVSS 10.0Dec 3, 2025Unauthenticated RCE via Flight protocol deserializationCVE-2025-55183CVSS 5.3Dec 11, 2025Source code exposure via error serializationCVE-2025-55184 / CVE-2025-67779High (DoS)Dec 11, 2025 (patch completed later)Malformed payload triggers server-side infinite loopCVE-2026-23864CVSS 7.5Jan 26, 2026Unbounded array size in Flight requests allows memory exhaustion
The mechanism connecting all four is the same: the Flight protocol has to deserialize data sent from the client to reconstruct server-side execution state, and that deserialization surface didn't exist before Server Components introduced it. The Pages Router has no Flight protocol and no Server Functions endpoint, so it was not exposed to this vulnerability class — not because it's more securely engineered, but because it doesn't have the architectural feature the vulnerabilities live in. That's a genuinely different kind of trade-off than a bundle-size percentage, and it belongs in the same conversation as the performance numbers for any team in a compliance-sensitive environment weighing migration timing, since it changes the calculus in a way that a Lighthouse score can't capture.
Turbopack Is a Third Variable Most Comparisons Don't Control For
Next.js 16 made Turbopack the default bundler for both routers, not just the App Router, and build-time improvements — commonly cited in the range of 2 to 5 times faster production builds — apply regardless of which router a project uses. One report on Turbopack's rollout across the ecosystem states Vercel's own production sites, including vercel.com and nextjs.org, now run on Turbopack in production, alongside a majority of local development sessions (https://www.buildmvpfast.com/blog/nextjs-app-router-vs-pages-router-saas-2026). The relevance here is methodological: a benchmark comparing "an App Router project on Next.js 16" against "a Pages Router project on Next.js 14" is measuring the bundler upgrade and the router change together, and attributing the combined gain to the router alone overstates the router's actual contribution. Any comparison worth trusting pins the Next.js version and bundler flag identically across both sides before touching routing.
Key Takeaways
- Bundle-size claims for the App Router are architecture-dependent, not automatic — the same framework has produced both a documented 42 percent increase and a 41 percent decrease depending entirely on where
'use client'boundaries were drawn. - Streaming via
<Suspense>is the App Router's most structurally reliable performance win, and it's concentrated on routes with slow, independent data dependencies rather than static pages. - TTFB and LCP figures attached to router migrations frequently bundle in a caching-strategy change at the same time; isolate that before crediting the router.
- The RSC Flight protocol introduced a CVSS 10.0 remote-code-execution surface (CVE-2025-66478) that the Pages Router structurally cannot have, since it has no Flight protocol or Server Function endpoints.
- Turbopack's build-speed gains apply to both routers under Next.js 16 and shouldn't be attributed to the App Router specifically in cross-version comparisons.
Read any App Router versus Pages Router benchmark with three questions in hand: how much of the page actually stayed server-rendered, whether the comparison holds the Next.js version and bundler constant on both sides, and whether the number being cited is a runtime figure or a build-time figure wearing a runtime figure's headline.





