SPA 의 initial bundle 이 5 MB 면 첫 페이지 로딩 5 초+. 그러나 사용자는 첫 페이지만 보면 됨. 나머지는 필요한 시점에 load — lazy loading. 이 가이드는 code splitting (route + dynamic), 이미지의 IntersectionObserver, prefetch vs preload 의 차이, 과도한 splitting 의 함정을 정리한다.
Code Splitting — 첫 chunk 작게
초기 (single bundle):
/bundle.js 3.0 MB → 한 번에 download + parse + execute
route-based splitting:
/home.js 0.3 MB
/dashboard.js 0.8 MB
/settings.js 0.2 MB
/admin.js 1.7 MB
/vendor.js 0.5 MB (공통 라이브러리)
→ home 페이지 진입 시 home.js + vendor.js 만 load (0.8 MB)
→ 다른 route 들은 click 시 lazy load
JS 측 syntax (modern bundler 모두 지원):
// 정적 import
import HomePage from "./HomePage";
// dynamic import (별도 chunk + 비동기)
const HomePage = lazy(() => import("./HomePage"));React Suspense + lazy
import {lazy, Suspense} from "react";
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
→ Dashboard 진입 시:
1. Suspense 가 <Spinner /> render (lazy 가 throw promise)
2. dynamic import 가 dashboard chunk download (~100-500ms)
3. resolve 후 Dashboard render
Next.js 의 dynamic 도 동일 패턴:
const Map = dynamic(() => import("./Map"), { ssr: false });Image Lazy Loading — IntersectionObserver
이미지 100 개의 페이지. 모두 즉시 download = 부피 큰 페이지 느림.
옛 방법 — scroll handler:
window.addEventListener("scroll", () => {
images.forEach(img => {
if (isInViewport(img)) loadImage(img);
});
});
→ 매 scroll event 마다 N images 계산 = 비싸 (jank)
modern — IntersectionObserver (browser native):
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, { rootMargin: "200px" }); // 200px 전에 미리 load
document.querySelectorAll("img[data-src]").forEach(img => observer.observe(img));
→ browser 가 직접 viewport 추적, application 의 scroll handler 0.
→ rootMargin 으로 "viewport 200px 전" 같은 prefetch margin.
HTML native 도 가능 (modern browser):
<img src="..." loading="lazy" />
→ 같은 IntersectionObserver 동작이 browser 내부에서Prefetch vs Preload vs Lazy
| 지시어 | 의도 | 예 |
|---|---|---|
<link rel="preload"> | "이 페이지에 곧 필요" → 즉시 download (high priority) | 위 페이지의 font, hero image |
<link rel="prefetch"> | "다음 페이지에 필요할 듯" → idle 시 download (low priority) | 다음 page 의 chunk |
<link rel="dns-prefetch"> | "이 도메인으로 곧 요청" → DNS 만 미리 | analytics, CDN domain |
<link rel="preconnect"> | "이 도메인 곧 사용" → DNS + TCP + TLS 미리 | API endpoint |
| lazy (loading="lazy") | "필요할 때만 load" — 반대 방향 | off-screen 이미지 |
Next.js / Nuxt 의 router 가 hover 또는 mouse-near link 시 자동 prefetch — modern framework 의 default.
과도한 Splitting 의 함정
너무 많은 chunk = TCP round-trip 다수
bundle 1 개 (3 MB): 1 connection → 3 MB stream
chunk 30 개 (각 100 KB): 30 connection (또는 HTTP/2 multiplex)
→ 각 chunk 의 download + parse 합
trade-off:
- HTTP/1.1: connection 비용 큼 → 적은 chunk 가 빠름
- HTTP/2 + multiplex: 여러 chunk 가능
- HTTP/3 + 0-RTT: 더 자유
권장:
- 너무 작게 X (chunk 당 최소 30-50 KB)
- 너무 크게 X (각 < 200-500 KB)
- vendor 별도 chunk + route 별 + 큰 components 분리Modern Frontend 의 lazy 패턴
React + Next.js:
1. App Router 의 route-level code split (자동)
2. dynamic(() => import(...)) 으로 component lazy
3. next/image 가 loading="lazy" 자동 + responsive
4. Suspense + Server Component 의 streaming (RSC)
Vue + Nuxt:
1. defineAsyncComponent + Suspense
2. <NuxtImg> 의 lazy + WebP / AVIF 자동 변환
3. route 별 chunk 자동
Vite:
- dev: native ESM (lazy by default, on-demand transform)
- prod: rollup 으로 production split
→ modern toolchain 의 default 는 이미 좋음. 더 깊은 split 은 measure 후만.흔한 함정
- 모든 component 를 lazy — 첫 paint 의 component 가 lazy 면 spinner 만 보임. above-the-fold 는 static import.
- 큰 chunk 가 첫 page 에 — admin 페이지 라이브러리 (chart, editor) 가 home bundle 에. webpack-bundle-analyzer 로 점검.
- preload 남용 — 모든 페이지에 preload 박으면 결과적 높은 priority 가 의미 X. 핵심 자원만.
- IntersectionObserver 의 rootMargin 0 — viewport 진입 후에 load 시작 → 사용자 한 박자 wait. rootMargin 100-300px.
- route prefetch 의 비용 — 모든 link 의 chunk 미리 download = 데이터 비용 ↑ (mobile). hover-prefetch 또는 visible link 만.
마무리
Lazy loading 의 본질 — "필요할 때 download". initial bundle 작게 → first paint 빠름. 그러나 chunk 너무 작으면 round-trip overhead.
실용 — modern framework default 활용 (Next.js / Nuxt 의 route split + dynamic + image lazy). monitor LCP / TTI / Total Bundle Size. 큰 component (chart, editor, map) 만 명시적 dynamic. prefetch 는 hover 또는 visible link 만.