Scaling with Vercel and Next.js 15: A Practical Guide
Next.js 15 and Vercel's infrastructure have changed how teams think about performance. Here's a practical guide to what actually moves the needle in production.
Next.js 15 landed with a set of changes that, on the surface, looked incremental. Turbopack stable. Partial pre-rendering in experimental release. Improved caching defaults. But in practice, the combination of these changes — alongside Vercel's evolving infrastructure — has meaningfully shifted how teams should think about application performance.
This is not a changelog walkthrough. It's a practical guide to the decisions that actually affect how fast your application feels and how well it holds up under load.
Understand the rendering model before you optimise
Most performance problems in Next.js applications come from using the wrong rendering strategy for the wrong content. Before reaching for optimisation tools, make sure you're making the right fundamental choices:
- Static pages are served from Vercel's edge network and cost essentially nothing in terms of compute. If your content doesn't change per-user or per-request, it should be static.
- Server components run on Vercel's serverless infrastructure. They're the right choice for content that is dynamic but not personalised — product listings, blog posts fetched from a CMS, public dashboards.
- Client components run in the browser. Keep them small and push them to the leaves of the component tree. A common mistake is making a parent component a client component because it needs one piece of interactivity, which unnecessarily pulls all its children into the client bundle.
Caching in Next.js 15
Next.js 15 made a significant change to caching defaults: fetch requests are now uncached by default. This is a breaking change from Next.js 13 and 14 behaviour, where requests were cached by default. If you upgraded without reading the release notes, this could explain unexpected increases in your API call volume.
The new model is more explicit and easier to reason about. Opt in to caching where you want it:
// Cache this request for 60 seconds
fetch(url, { next: { revalidate: 60 } })
// Cache indefinitely, revalidate on demand
fetch(url, { next: { tags: ['products'] } })For data that truly needs to be fresh on every request, the new behaviour is correct by default. For mostly-static data, explicit revalidation gives you precise control over the freshness/performance tradeoff.
Partial Pre-rendering
PPR is the most architecturally significant feature in Next.js 15, even though it's still experimental. The idea: a page can have a static shell that is served instantly from the edge, with dynamic sections that stream in from the server.
In practice, this means you can have a product page where the layout, navigation, and static content are served in milliseconds, while the personalised recommendations and real-time inventory data stream in behind a Suspense boundary. Users see meaningful content immediately rather than waiting for the slowest dynamic piece to resolve.
To use PPR today, opt in at the route level and wrap dynamic content in Suspense:
export const experimental_ppr = true;
export default function ProductPage() {
return (
<main>
<StaticProductDetails />
<Suspense fallback={<RecommendationsSkeleton />}>
<DynamicRecommendations />
</Suspense>
</main>
);
}Image and font optimisation
Next.js's built-in Image component handles the most impactful image optimisation automatically: format conversion to WebP/AVIF, responsive sizing, and lazy loading. The mistakes we most commonly see in production apps:
- Using
fillwithout setting a defined size on the parent container, causing layout shift. - Not setting
priorityon the largest contentful paint image, leaving it to load lazily when it should load eagerly. - Importing images from
public/without using theImagecomponent, missing all the optimisation benefits.
For fonts, next/font eliminates layout shift from web fonts by inlining font metrics at build time. If you're still loading fonts from Google Fonts directly via a <link> tag, switch to next/font/google — it's a one-line change that typically improves CLS scores noticeably.
Edge functions vs serverless functions
Vercel gives you two compute options: serverless functions that run in a specific region, and edge functions that run globally close to users. Edge functions have lower latency but a restricted runtime — no Node.js APIs, limited package support.
The right mental model: use edge functions for operations that are latency-sensitive and computationally simple — authentication checks, A/B testing logic, geolocation-based redirects. Use serverless functions for operations that need full Node.js — database queries, complex data transformations, third-party API calls with heavy dependencies.
Running database queries in edge functions is a common mistake. The query itself will be fast, but unless your database is globally distributed (PlanetScale, Neon, Turso), the network round-trip to your single-region database will be slower from the edge than from a co-located serverless function.
Monitoring what matters
Vercel's Analytics and Speed Insights give you real-user Core Web Vitals data broken down by page and geography. The metrics worth watching most closely: LCP (Largest Contentful Paint) for perceived load speed, INP (Interaction to Next Paint) for responsiveness, and CLS (Cumulative Layout Shift) for visual stability.
Performance work without measurement is guessing. Set baseline numbers before you start optimising, measure the impact of each change, and prioritise the pages that real users actually visit most.
Nogeybix Labs
Full-stack software & AI engineering team based in Nairobi, building intelligent products for founders globally.
Building something?
We'd love to hear about your project.

