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:
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:
// 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:
next/fontfor self-hosted fonts. No external requests to Google Fonts.display: swapto prevent invisible text during font loading.- 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.
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:
: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/linkfor 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.