Menu

Next.js 15 Performance Optimization: A Complete Guide
May 28, 2025Development8 min read

Next.js 15 Performance Optimization: A Complete Guide

J
Joseph Maina

Next.js 15 introduced several game-changing features for performance optimization. After migrating three production apps and measuring the results, here's what actually works.

Server Components: The Real Impact

Server Components aren't just hype. In our e-commerce app, switching the product catalog to Server Components reduced the initial JavaScript bundle from 247KB to 156KB - a 37% decrease.

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Before: Client Component
'use client'
import { useEffect, useState } from 'react'
export default function ProductList() {
const [products, setProducts] = useState([])
useEffect(() => {
fetch('/api/products').then(res => res.json()).then(setProducts)
}, [])
return (
{products.map(product => )}
)
}
text
1
2
3
4
5
6
7
8
9
10
11
12
13
// After: Server Component
import { db } from '@/lib/database'
export default async function ProductList() {
const products = await db.products.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' }
})
return (
{products.map(product => )}
)
}

Streaming with Suspense: Measured Results

Implementing streaming reduced our Time to First Byte (TTFB) by 43% and improved Largest Contentful Paint (LCP) scores significantly.

text
1
2
3
4
5
6
7
8
9
10
11
12
import { Suspense } from 'react'
import { ProductListSkeleton } from '@/components/skeletons'
export default function ProductPage() {
return (

Our Products

}>
)
}

New Caching Strategy in Next.js 15

The new unstable_cache API gives you granular control over caching. Here's how we cache expensive database queries:

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/database'
const getCachedProducts = unstable_cache(
async (category: string) => {
return await db.products.findMany({
where: { category },
include: { images: true, reviews: true }
})
},
['products-by-category'],
{ revalidate: 3600, tags: ['products'] }
)
export default async function CategoryPage({ params }: { params: { category: string } }) {
const products = await getCachedProducts(params.category)
return
}

Image Optimization Wins

Using Next.js Image component with the new placeholder="blur" and blurDataURL improved our Cumulative Layout Shift (CLS) score from 0.25 to 0.05.

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Image from 'next/image'
import { generateBlurDataURL } from '@/lib/blur'
export function ProductImage({ src, alt }: { src: string, alt: string }) {
return (
src={src}
alt={alt}
width={400}
height={300}
placeholder="blur"
blurDataURL={generateBlurDataURL()}
className="object-cover rounded-lg"
priority // For above-the-fold images
/>
)
}

Bundle Analysis Results

Before optimization:

  • First Load JS: 247KB
  • LCP: 2.8s
  • CLS: 0.25
  • FID: 145ms

After optimization:

  • First Load JS: 156KB
  • LCP: 1.6s
  • CLS: 0.05
  • FID: 89ms

Real-World Monitoring

We use Vercel Analytics and a custom performance dashboard built with Grafana to track these metrics in production. The key is measuring before and after each optimization.

Performance optimization isn't a one-time task—it's an ongoing process. Start with the biggest wins (Server Components and proper caching) and measure everything.

Share this article