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.
1234567891011121314// 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 => )})}
12345678910111213// After: Server Componentimport { 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.
123456789101112import { 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:
12345678910111213141516import { 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.
12345678910111213141516import 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.