---
title: "Next.js 15 on Vercel: Performance Patterns That Matter"
description: "Performance optimization patterns for Next.js 15 deployed on Vercel - from React Server Components to image optimization and edge middleware."
date: 2025-11-09T00:00:00.000Z
category: Engineering
readingTime: "3 min read"
---


This site runs on Next.js 15 deployed to Vercel. The combination is powerful, but getting the best performance requires understanding the patterns that actually matter - and ignoring the ones that don't.

## React Server Components: The Default That Changes Everything

Next.js 15 defaults to Server Components. This isn't just a rendering strategy - it's a fundamental shift in how you think about data and UI.

Server Components render on the server and send HTML to the client. No JavaScript bundle for the component itself. No hydration cost. For a content-heavy site like this one, that means:

- **Blog posts** render entirely on the server. The markdown parsing, syntax highlighting, and HTML generation happen once, at build time. The client receives pure HTML.
- **Navigation** is a Server Component that reads the route list from a constants file. No client-side state needed.
- **The page shell** - header, footer, layout - is all server-rendered. Zero client JavaScript for the structure.

The client components on this site are minimal: theme toggling, mobile navigation state, and Alpine.js interactions. Everything else is server-only.

## Static Generation for Content

For a personal site with blog posts, Static Site Generation (SSG) is the obvious choice. Every page is pre-rendered at build time:

```typescript
export async function generateStaticParams() {
  const posts = getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}
```

Build time generates HTML for every blog post. Vercel caches the output at the edge. First-byte time approaches the theoretical minimum - it's just serving a cached file from the nearest CDN node.

There's no server computation on page load. No database queries. No API calls. Just cached HTML.

## Image Optimization

Vercel's image optimization pipeline (`next/image`) handles responsive images, format conversion (WebP/AVIF), and lazy loading. But the biggest performance win is simpler: **don't use large images**.

For this site, I follow a strict image budget:

- **Hero images:** Max 200KB, served as WebP
- **Blog post images:** Max 100KB, always below the fold with `loading="lazy"`
- **Author photos:** Max 30KB, heavily compressed

The `next/image` component handles the technical optimization, but the discipline of keeping source images small matters more than any automatic optimization.

## Edge Middleware for Redirects

This site has gone through several URL structure changes. Old links still exist in the wild - bookmarks, search engine indexes, other sites linking to me. Edge middleware handles redirects without hitting the origin:

```typescript
// middleware.ts
export function middleware(request: NextRequest) {
  const redirects: Record<string, string> = {
    '/old-path': '/new-path',
    '/legacy/about': '/',
  };

  const redirect = redirects[request.nextUrl.pathname];
  if (redirect) {
    return NextResponse.redirect(new URL(redirect, request.url), 301);
  }
}
```

Redirects execute at the edge - the nearest Vercel CDN node to the user. Latency is negligible because the request never reaches the origin server.

## Font Loading Strategy

Fonts are a common performance bottleneck. A badly loaded custom font can cause layout shifts and delays. My approach:

1. **`next/font`** for self-hosted fonts. No external requests to Google Fonts.
2. **`display: swap`** to prevent invisible text during font loading.
3. **Subset fonts.** I only include the character sets I actually use. For a site with English content, that means dropping CJK characters and reducing the font file from ~200KB to ~30KB.

```typescript
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
});
```

## CSS Performance

This site uses vanilla CSS with custom properties - no CSS-in-JS, no Tailwind, no runtime styling. The entire stylesheet is about 15KB (minified, before gzip).

CSS custom properties enable theming without JavaScript:

```css
:root {
  --color-bg-primary: #ffffff;
  --color-text-primary: #1d1d1f;
}

[data-theme="dark"] {
  --color-bg-primary: #000000;
  --color-text-primary: #f5f5f7;
}
```

Theme switching changes one `data-theme` attribute. Every component that references custom properties updates instantly. No style recalculation, no rerender, no flash.

## Build-Time Computation

Everything that can be computed at build time should be. For this blog:

- **Reading time calculation** - computed when parsing markdown, not at render time
- **Related posts** - cross-referenced by tag overlap at build time
- **Table of contents** - extracted from headings during markdown parsing
- **RSS feed** - generated as a static route
- **Sitemap** - generated from all known routes

The build runs once. Users never wait for computation.

## What I Don't Optimize

Performance optimization has diminishing returns. I deliberately skip:

- **Code splitting per blog post** - they're statically generated. There's no JavaScript to split.
- **Service worker caching** - Vercel's CDN handles caching better than a service worker would.
- **Preloading every link** - I use `next/link` for route prefetching, which handles this automatically on hover.

The best performance optimization is often removing complexity, not adding it.

## The Numbers

Lighthouse scores for this site:

- **Performance:** 99-100
- **Accessibility:** 100
- **Best Practices:** 100
- **SEO:** 100

These scores are achievable because the architecture is simple. Server Components + SSG + edge caching + minimal client JavaScript. No framework fighting needed.

---

*Building a personal site with Next.js? Use Server Components for everything, add client components only where you need interactivity, and let Vercel's edge do the heavy lifting.*
