Next.js는 기본 설정만으로도 꽤 빠르지만, 프로덕션 규모에서는 번들 크기와 렌더링 전략을 직접 제어해야 한다. 잘못 설계된 import 구조 하나가 초기 로드 시간을 수백 밀리초 늘릴 수 있다. 이 글은 실제로 효과 있는 최적화 기법만 다룬다.
Next.js는 페이지 단위로 Code Splitting을 자동 적용한다. 하지만 같은 페이지 내에서도 무거운 컴포넌트는 dynamic import로 분리해야 한다.
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading...</p>,
ssr: false,
})
ssr: false는 서버에서 렌더링하지 않아도 되는 클라이언트 전용 컴포넌트에만 사용한다. 무분별하게 적용하면 오히려 Hydration 비용이 커진다.
Code Splitting: 하나의 번들을 여러 청크로 나눠 필요한 시점에만 로드하는 기법. 초기 번들 크기를 줄여 TTI(Time to Interactive)를 단축한다.
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({})
ANALYZE=true npm run build
실행하면 브라우저에서 각 청크의 크기를 시각적으로 확인할 수 있다. 가장 먼저 볼 것은 node_modules 안에서 예상보다 큰 라이브러리다.
번들 구조 예시
──────────────────────────────────
pages/_app [12 kB]
pages/index [8 kB]
chunks/
└─ moment.js [67 kB] ← 문제
└─ chart.js [41 kB]
──────────────────────────────────
moment.js처럼 대체재가 있는 라이브러리는 date-fns나 dayjs로 교체하는 것이 가장 빠른 해결책이다.
Next.js의 <Image> 컴포넌트는 자동으로 WebP 변환, 지연 로딩, 크기 최적화를 처리한다.
import Image from 'next/image'
<Image
src="/hero.png"
alt="hero"
width={1200}
height={630}
priority // LCP 대상 이미지에만 사용
/>
LCP(Largest Contentful Paint) 대상이 되는 첫 화면 이미지에만 priority를 붙인다. 모든 이미지에 붙이면 효과가 없다.
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
next/font는 빌드 타임에 폰트를 다운로드해 self-hosting으로 제공한다. Google Fonts 요청이 사라지므로 외부 네트워크 의존성이 제거된다.
display: 'swap': 폰트 로딩 전에 fallback 폰트를 먼저 보여주는 CSS font-display 전략. FOIT(Flash of Invisible Text)를 방지한다.
Next.js 13.1부터 도입된 Turbopack은 Webpack을 대체하는 Rust 기반 번들러다.
next dev --turbo
개발 서버 기동 속도와 HMR 속도가 크게 개선된다. 프로덕션 빌드는 아직 안정화 중이므로 next dev 단계에서만 사용하는 것이 현재 권장 방식이다.
| 항목 | Webpack | Turbopack |
|---|---|---|
| 언어 | JavaScript | Rust |
| 개발 서버 HMR | 보통 | 빠름 |
| 프로덕션 빌드 | 안정 | 실험적 |
| 플러그인 생태계 | 풍부 | 제한적 |