ResourcesWeb PerformanceNext.js Performance: 7 Changes That Made My App 3x Faster
Web PerformanceNext.js Performance: 7 Changes That Made My App 3x Faster7 min

Next.js Performance: 7 Changes That Made My App 3x Faster

Real Next.js performance wins, from Server Components to image optimization, with measurable impact on Core Web Vitals.

📅January 19, 2026TechTwitter.ionextjsweb-performancereactoptimization

The Starting Point

A Next.js 15 app with decent code, no major bugs, but poor Core Web Vitals: LCP at 5.2s, INP at 380ms. Here are the 7 changes that got it to LCP 1.4s and INP 65ms.


1. Move Data Fetching to Server Components

The biggest win. Client components that fetch data on load add a waterfall:

Browser → HTML → JS bundle → component mounts → fetch → render

Server Components collapse this:

Browser → HTML (already has data) → render
// Before: Client component with useEffect
'use client'
export default function ProductList() {
  const [products, setProducts] = useState([])
  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts)
  }, [])
  return <Grid items={products} />
}

// After: Server component — data fetched at build/request time
export default async function ProductList() {
  const products = await db.products.findMany()
  return <Grid items={products} />
}

Impact: LCP dropped from 5.2s to 2.8s — eliminated an entire render waterfall.


2. Use next/image Correctly

Next.js's Image component handles optimization, but only if you use it right:

import Image from 'next/image'

// Wrong: no priority, unknown size
<Image src="/hero.jpg" alt="Hero" />

// Right: priority for above-fold, explicit dimensions
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority  // <-- critical: preloads this image
  quality={85}
/>

The priority prop is the key: it adds <link rel="preload"> for the image and removes loading="lazy".

Impact: LCP from 2.8s to 1.8s.


3. Lazy Load Below-the-Fold Components

Heavy components below the fold shouldn't block initial load:

import dynamic from 'next/dynamic'

// Lazy load with no SSR for browser-only heavy components
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,
})

// Lazy load for components below the fold
const ReviewsSection = dynamic(() => import('./ReviewsSection'))

Impact: Bundle size reduced by 40KB gzipped, TTI improved.


4. Optimize Fonts with next/font

Google Fonts loaded via <link> block rendering. next/font downloads and self-hosts fonts, eliminating the external request:

// app/layout.tsx
import { Inter } from 'next/font/google'

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

export default function Layout({ children }) {
  return (
    <html className={inter.className}>
      {children}
    </html>
  )
}

Impact: Eliminated 200ms font load time, reduced CLS from 0.18 to 0.04.


5. Add Proper Caching Headers

Next.js App Router has granular caching control:

// Static data — cache until revalidated
export const revalidate = 3600 // revalidate every hour

// Dynamic data — no cache
export const dynamic = 'force-dynamic'

// Per-fetch caching
const products = await fetch('/api/products', {
  next: { revalidate: 300 } // cache for 5 minutes
})

For API routes:

export async function GET() {
  const data = await getStaticData()
  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400'
    }
  })
}

Impact: Repeat visits went from 1.4s LCP to 0.3s (served from cache/CDN).


6. Reduce Client-Side JavaScript

Audit what's in your bundle:

# Analyze bundle
ANALYZE=true next build

Common culprits:

  • Large date libraries (moment.jsdate-fns or Temporal)
  • Full icon libraries (import only what you use)
  • Unused 'use client' components that could be Server Components
// Before: entire icon library in client bundle
import { FiArrowRight, FiCheck } from 'react-icons/fi'

// After: direct import
import FiArrowRight from 'react-icons/fi/FiArrowRight'
import FiCheck from 'react-icons/fi/FiCheck'

Impact: 60KB JS reduction → INP from 380ms to 95ms.


7. Use Partial Prerendering (PPR)

Next.js 15 introduced Partial Prerendering: static shell served instantly, dynamic parts streamed in.

// app/page.tsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <main>
      {/* Static — served immediately from edge */}
      <StaticHero />
      <StaticNav />

      {/* Dynamic — streams in after static shell */}
      <Suspense fallback={<ProductSkeleton />}>
        <DynamicProductList />
      </Suspense>
    </main>
  )
}
// next.config.js
module.exports = {
  experimental: { ppr: true }
}

Impact: LCP from 1.8s to 1.4s — users see static content instantly.


Key Takeaways

  1. Server Components — biggest win for data-heavy pages
  2. next/image with priority — critical for LCP
  3. Dynamic imports — lazy load everything below the fold
  4. next/font — self-host fonts, eliminate external requests
  5. Caching — aggressive caching is free performance
  6. Bundle analysis — audit and remove heavy dependencies
  7. PPR — stream dynamic content after the static shell