Next.js 15 Performance: Server Components, Caching & Core Web Vitals
A practical guide to squeezing maximum performance from Next.js 15 — React Server Components, granular caching strategies, streaming Suspense boundaries, image optimization, and hitting 100 on Lighthouse.
The Mental Model Shift
Next.js 15 with React 19 changes how you think about performance. The default is now "render on the server, hydrate nothing unless necessary." Every component is a Server Component by default. You add 'use client' when the component actually needs interactivity.
This means most of your code runs zero JavaScript on the client.
Server Components vs Client Components
// app/blog/page.tsx — Server Component (default)
// Fetches data at request time, sends HTML to client
// Zero JavaScript bundle impactimport { getBlogPosts } from '@/lib/blog'
export default async function BlogPage() { const posts = await getBlogPosts() // Direct DB/API call — no useEffect
return (
// components/SearchBox.tsx — Client Component
// Needs useState, useEffect, event handlers'use client' import { useState } from 'react'
export function SearchBox({ onSearch }: { onSearch: (q: string) => void }) { const [query, setQuery] = useState('') return ( { setQuery(e.target.value); onSearch(e.target.value) }} /> ) } ```
Granular Caching
// Cache for 1 hour, revalidate on demand
const posts = await fetch('/api/posts', {
next: { revalidate: 3600, tags: ['blog-posts'] }
})// Force-dynamic (never cache) const userData = await fetch('/api/me', { cache: 'no-store' })
// Revalidate specific tags from a Server Action import { revalidateTag } from 'next/cache' export async function publishPost(slug: string) { await db.update({ slug, published: true }) revalidateTag('blog-posts') // Clears all caches tagged 'blog-posts' } ```
Streaming Suspense Boundaries
import { Suspense } from 'react'export default function DashboardPage() { return (
Dashboard
{/ Fast: renders immediately /}
{/ Slow: streams in when ready, shows skeleton first /}
Image Optimization
import Image from 'next/image'// Always use next/image — automatic WebP/AVIF conversion + lazy loading
Core Web Vitals Checklist
| Metric | Target | Common Fix |
|---|---|---|
| LCP | < 2.5s | Preload hero image, use priority prop |
| FID / INP | < 200ms | Move heavy work to Server Components |
| CLS | < 0.1 | Always set width/height on images |
| TTFB | < 800ms | Use edge runtime for dynamic routes |