본문으로 건너뛰기 Next.js Performance Optimization Guide | Core Web Vitals

Next.js Performance Optimization Guide | Core Web Vitals

Next.js Performance Optimization Guide | Core Web Vitals

이 글의 핵심

Slow Next.js apps lose users and rank lower in search. This guide covers the most impactful performance optimizations — from bundle size reduction to image optimization and caching strategies — with real measurement techniques.

Why Next.js Performance Matters

A 100ms increase in load time reduces conversion by 1%. Core Web Vitals directly affect Google rankings. And with Next.js’s rich feature set, it’s easy to accidentally ship a slow app.

This guide focuses on measurement first, optimization second — never guess.


1. Measure First

# Build and analyze
npm run build

# Check what's included in your bundle
ANALYZE=true npm run build
# (requires @next/bundle-analyzer)

# Real-user metrics
# Next.js Speed Insights in vercel.com dashboard
# Or: Google Search Console → Core Web Vitals

Install bundle analyzer

npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // your config
})
ANALYZE=true npm run build
# Opens treemap of bundle contents in browser

2. Image Optimization

Images are the #1 LCP killer. Always use next/image:

import Image from 'next/image'

// ❌ Bad: regular img tag
<img src="/hero.jpg" alt="Hero" style={{ width: '100%' }} />

// ✅ Good: next/image with correct props
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={600}
  priority           // LCP image: load immediately, don't lazy-load
  quality={85}       // 75-85 is the sweet spot
  placeholder="blur" // show blurred placeholder while loading
  blurDataURL="data:image/jpeg;base64,..."  // tiny base64 preview
/>

// Below-the-fold images: lazy load (default)
<Image
  src="/product.jpg"
  alt="Product"
  width={400}
  height={400}
  // priority NOT set → lazy loaded automatically
/>

Remote images

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'images.unsplash.com' },
      { protocol: 'https', hostname: 'cdn.yoursite.com' },
    ],
    formats: ['image/avif', 'image/webp'],  // serve AVIF first, WebP fallback
  },
}

3. Bundle Size Reduction

Dynamic imports (code splitting)

import dynamic from 'next/dynamic'

// Heavy component loaded only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false,  // client-only (e.g., uses window)
})

// Heavy library (e.g., markdown editor, date picker)
const MarkdownEditor = dynamic(() => import('react-md-editor'), { ssr: false })

// Only load when user interacts
const [showModal, setShowModal] = useState(false)
const Modal = dynamic(() => import('./HeavyModal'))

return (
  <>
    <button onClick={() => setShowModal(true)}>Open Modal</button>
    {showModal && <Modal onClose={() => setShowModal(false)} />}
  </>
)

Replace heavy dependencies

# Before replacing, check your bundle
ANALYZE=true npm run build

# Common replacements
lodash lodash-es (tree-shakeable) or individual functions
moment date-fns or dayjs (~80% smaller)
axios native fetch (zero bytes)
react-icons @phosphor-icons/react (tree-shakeable)
// ❌ Imports entire lodash
import _ from 'lodash'
const sorted = _.sortBy(users, 'name')

// ✅ Tree-shakeable import
import { sortBy } from 'lodash-es'
const sorted = sortBy(users, 'name')

Analyze and remove unused code

# Find unused exports
npx knip

# Check package sizes before installing
npx bundlephobia <package-name>

4. Caching Strategies (App Router)

// Static (cached forever, revalidated on deploy)
export const dynamic = 'force-static'

// Static with time-based revalidation (ISR)
export const revalidate = 3600  // revalidate every hour

// Dynamic (no cache, runs on every request)
export const dynamic = 'force-dynamic'

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

// No cache for this specific fetch
const user = await fetch('/api/user', {
  cache: 'no-store'
})

// Revalidate on demand (after form submit, admin action)
import { revalidatePath, revalidateTag } from 'next/cache'

async function publishPost(postId: string) {
  await db.posts.update({ id: postId, published: true })
  revalidatePath('/blog')          // revalidate the blog listing
  revalidatePath(`/blog/${postId}`) // revalidate this specific post
}

5. Reducing LCP (Largest Contentful Paint)

LCP measures when the largest element is visible. Target: < 2.5s.

// ✅ Preload LCP image
import { headers } from 'next/headers'

// In layout.tsx or page.tsx
export default function Layout({ children }) {
  return (
    <html>
      <head>
        <link
          rel="preload"
          as="image"
          href="/hero.jpg"
          // For responsive images:
          imageSrcSet="/hero-800.jpg 800w, /hero-1200.jpg 1200w"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

// ✅ Priority on LCP image
<Image src="/hero.jpg" priority alt="Hero" width={1200} height={600} />

// ✅ Preconnect to external image domains
<link rel="preconnect" href="https://images.unsplash.com" />

6. Preventing CLS (Cumulative Layout Shift)

CLS measures visual stability. Target: < 0.1.

// ❌ Image without dimensions causes CLS
<img src="/logo.png" alt="Logo" />

// ✅ Always specify dimensions
<Image src="/logo.png" alt="Logo" width={120} height={40} />

// ❌ Dynamic content loading without placeholder
{user ? <UserAvatar user={user} /> : null}

// ✅ Reserve space while loading
<div style={{ width: 40, height: 40 }}>
  {user ? <UserAvatar user={user} /> : <Skeleton variant="circular" />}
</div>

// ❌ Web font without fallback (text shifts when font loads)
// ✅ Use next/font (automatic font optimization)
import { Inter } from 'next/font/google'
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
})

7. Reducing INP (Interaction to Next Paint)

INP measures responsiveness to clicks and inputs. Target: < 200ms.

// ❌ Heavy synchronous computation blocks the main thread
function handleSearch(query: string) {
  const results = heavySearch(allProducts, query)  // blocks UI
  setResults(results)
}

// ✅ Defer non-critical work
import { startTransition } from 'react'

function handleSearch(query: string) {
  setQuery(query)  // urgent: update input immediately
  startTransition(() => {
    setResults(heavySearch(allProducts, query))  // defer: can be interrupted
  })
}

// ✅ Use useDeferredValue for expensive renders
const deferredQuery = useDeferredValue(query)
const results = useMemo(() => search(deferredQuery), [deferredQuery])

// ✅ Move heavy work to web workers
const worker = new Worker(new URL('./search.worker.ts', import.meta.url))
worker.postMessage({ query })
worker.onmessage = (e) => setResults(e.data)

8. Server Components vs Client Components

Getting this boundary right is crucial for performance:

// ✅ Server Component (default in App Router)
// - Zero JS sent to client
// - Can directly access DB/API
// - Can't use useState, useEffect, event handlers

async function PostList() {
  const posts = await db.posts.findMany()  // direct DB access
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

// ✅ Client Component (add 'use client' only when needed)
// - Has interactivity (useState, useEffect, events)
// - JS is sent to client
'use client'
function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('')
  return <input value={query} onChange={e => { setQuery(e.target.value); onSearch(e.target.value) }} />
}

// ✅ Optimal: Server Component wraps Client Component
async function SearchPage() {
  const initialPosts = await db.posts.findMany()  // server data
  return (
    <div>
      <SearchInput onSearch={handleSearch} />  // client interaction
      <PostList posts={initialPosts} />        // server data, no JS
    </div>
  )
}

9. Next.js Config Optimizations

// next.config.js
module.exports = {
  // Compress responses
  compress: true,

  // Strict mode catches issues early
  reactStrictMode: true,

  // Experimental optimizations
  experimental: {
    optimizePackageImports: ['@mui/material', 'lucide-react', 'date-fns'],
    // ↑ Tree-shake these packages automatically
  },

  // Headers for caching static assets
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
      },
    ]
  },
}

Performance Checklist

□ Run Lighthouse audit (target: 90+ on all scores)
□ Check Core Web Vitals in Google Search Console
□ Use next/image for ALL images — no <img> tags
□ Set priority on the LCP image
□ Dynamic import for components > 50KB (charts, editors, maps)
□ Add revalidate to data fetches (avoid force-dynamic where possible)
□ Use next/font — eliminate font-related CLS
□ Check bundle analyzer — nothing over 100KB that you don't expect
□ All images have explicit width/height — no CLS
□ Use startTransition for non-urgent state updates
□ Run 'npx knip' — remove unused code

Key Takeaways

MetricKey fix
LCPnext/image with priority, preconnect
CLSImage dimensions, next/font, skeleton placeholders
INPstartTransition, defer heavy computation
Bundle sizeDynamic imports, replace heavy libraries
Time to First ByteStatic/ISR instead of SSR, CDN caching

Measure with Lighthouse and Google Search Console, fix the highest-impact items first, and re-measure. The biggest wins — images, bundle size, correct rendering mode — are often 5-line changes that cut load time in half.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Optimize Next.js apps for speed. Covers Core Web Vitals, Image optimization, bundle analysis, lazy loading, caching stra…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Next.js, Performance, Core Web Vitals, React, SEO, Web.