JavaScript는 웹에서 가장 비싼 리소스입니다 — 바이트당 JavaScript 파일은 동일한 크기의 이미지보다 비용이 많이 드는데, 디코딩만 하면 되는 게 아니라 파싱과 실행이 필요하기 때문입니다. 500KB JavaScript 번들은 중급 폰에서 메인 스레드를 2~4초 동안 차단할 수 있습니다. 100KB 미만으로 줄이는 방법을 알아봅니다.
1단계: 현재 상태 측정
최적화 전에 현재 번들을 감사하세요. npx @next/bundle-analyzer를 실행하세요.next.config.js에서 ANALYZE=true npm run build로 활성화합니다. 트리맵은 어떤 패키지가 가장 크고, 어떤 페이지가 어떤 모듈을 포함하는지 보여줍니다.
2단계: 사용하지 않는 의존성 제거
가장 흔한 번들 비대화 소스:
| 패키지 | 크기 | 대체 | 대체 크기 |
|---|---|---|---|
| moment.js | 67 KB | date-fns (트리쉐이킹) | 3–8 KB |
| lodash | 72 KB | lodash-es (트리쉐이킹) | 1–5 KB |
| axios | 14 KB | fetch (네이티브) | 0 KB |
| react-icons (전체) | 340 KB | 개별 SVG 임포트 | 0.5 KB/아이콘 |
3단계: 트리쉐이킹
트리쉐이킹은 한 번도 임포트되지 않은 익스포트를 제거합니다. 작동하려면:
- 배럴 파일에서 기본 임포트 대신 이름 임포트를 사용하세요.
import { format } from 'date-fns'는 트리쉐이킹됩니다;import dateFns from 'date-fns'는 안 됩니다. - 유틸리티 모듈에서 사이드 이펙트 임포트를 피하세요. 상단에
import 'some-polyfill'이 있는 파일은 사이드 이펙트가 있다고 처리되어 트리쉐이킹되지 않습니다. - 라이브러리의 package.json에서
sideEffects: false를 확인하세요. 이를 선언하지 않은 라이브러리는 Webpack/Turbopack의 트리쉐이킹에서 제외됩니다.
4단계: 코드 스플리팅
Next.js는 페이지 수준에서 자동으로 코드를 분리합니다. 서브 페이지 분리에는 동적 임포트를 사용하세요:
const HeavyEditor = dynamic(() => import('./Editor'), { loading: () => <Spinner /> });
동적 임포트 좋은 후보: 리치 텍스트 에디터, 차트 라이브러리, PDF 뷰어, 지도 컴포넌트, 비디오 플레이어, 탭이나 모달 뒤에 있어 초기 로드 시 보이지 않는 컴포넌트.
5단계: 서드파티 스크립트 최적화
Next.js의 <Script> 컴포넌트를 서드파티 스크립트에 사용하세요:
strategy="lazyOnload"— 다른 모든 것 이후에 로드, INP에 영향 없음.strategy="afterInteractive"— 하이드레이션 후 로드, 대부분의 분석에 적합.strategy="worker"— Web Worker에서 로드(실험적), 메인 스레드에서 완전히 분리.
전후 비교: 실제 수치
| 지표 | 이전 | 이후 |
|---|---|---|
| 홈페이지 JS (gzipped) | 520 KB | 82 KB |
| 메인 스레드 차단 시간 | 3.2초 | 0.4초 |
| INP (75번째 백분위수) | 480 ms | 95 ms |
| Lighthouse 성능 | 61 | 94 |
변경 사항: moment.js를 date-fns로, lodash를 네이티브 JS로 교체하고, 데이터 테이블을 서버 컴포넌트로 이동하고, 분석 스크립트를 지연 로드했습니다.