Next.js 번들 사이즈 줄이기 — bundle-analyzer, dynamic import
배경
엘림 사내 안전진단 웹 툴을 운영하면서, 배포 후 페이지 초기 로딩이 느리다는 피드백이 있었다.
코드를 눈으로 봐선 병목을 찾을 수 없어서, @next/bundle-analyzer로 번들을 직접 들여다봤다.
뭐가 문제였나
트리맵을 펼쳐보니 눈에 띄는 덩어리가 바로 보였다.
@fullcalendar— 트리맵에서 가장 큰 블록, 캘린더가 없는 페이지에도 포함konva— 캔버스 라이브러리, 캔버스가 없는 페이지에서도 포함jszip— 6개 파일 최상단에 정적 import, 실제 사용은 다운로드 버튼 클릭 시뿐- 탭 컴포넌트들 — 현재 탭이 아닌 것까지 초기 로드 시 한꺼번에 불려오고 있었음
전부 "지금 당장 필요하지 않은데 처음부터 다 불러오고 있는" 케이스였다.
해결 1: FullCalendar — dynamic import
// Before
import Calendar from './Calendar'
// After
const Calendar = dynamic(() => import('./Calendar'), { ssr: false })
FullCalendar는 window에 의존하는 로직이 있어서 ssr: false를 붙였다.
해결 2: 탭 컴포넌트 — dynamic import
// Before
import MachineProjectTabContent from './_components/tabs/MachineProjectTabContent'
import PictureListTabContent from './_components/tabs/PictureListTabContent'
// After
const MachineProjectTabContent = dynamic(() => import('./_components/tabs/MachineProjectTabContent'))
const PictureListTabContent = dynamic(() => import('./_components/tabs/PictureListTabContent'))
탭을 클릭하기 전까지 해당 컴포넌트의 JS를 다운로드하지 않는다.
해결 3: jszip — 함수 내 동적 import
// Before
import JSZip from 'jszip'
// After
async function downloadFiles() {
const { default: JSZip } = await import('jszip')
// ...
}
6개 파일 모두 동일한 패턴으로 수정했다.
{ default: JSZip } 구조 분해는 jszip이 CommonJS 모듈이라서 필요하다.
해결 4: 이미지 — loading='lazy'
<img loading='lazy' src={pic.presignedUrl} alt={pic.originalFileName} />
사진이 많은 목록 페이지에서, HTML 속성 하나로 화면에 보이는 이미지만 우선 다운로드하게 됐다.
결과
| 페이지 | 최적화 전 | 최적화 후 | 감소량 |
|--------|-----------|-----------|--------|
| /machine/[id] | 470 kB | 249 kB | -221 kB (-47%) |
| /calendar | 365 kB | 169 kB | -196 kB (-54%) |
기준은 하나였다 — "이 모듈이 페이지 첫 진입 시점에 반드시 필요한가?"
아니라면 dynamic import 후보다.