--- id: wiki-2026-0508-lazy-loading-strategies title: Lazy Loading Strategies category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Lazy Load, Deferred Loading, On-Demand Loading] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [frontend, performance, web, react, intersection-observer] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: react --- # Lazy Loading Strategies ## 매 한 줄 > **"매 필요한 순간에만 로드"**. 초기 bundle/네트워크 비용을 줄여 LCP, TTI 개선. Image, route, component, data 모두 적용 가능. ## 매 핵심 ### 매 4가지 layer 1. **Image lazy loading**: `loading="lazy"`, IntersectionObserver, blur placeholder. 2. **Code splitting**: dynamic `import()`, route-based, component-based. 3. **Data lazy loading**: virtual scroll, infinite scroll, pagination. 4. **Resource hints**: `prefetch`, `preload`, priority hints. ### 매 트리거 - viewport 진입 (IntersectionObserver). - user interaction (click, hover). - idle (`requestIdleCallback`). - route navigation. ### 매 측정 - **LCP**: Largest Contentful Paint < 2.5s. - **CLS**: lazy image 로 인한 layout shift 방지 (width/height 명시). - **JS bundle size**: route 별 < 200KB gzip 권장. ## 💻 패턴 ### Native image lazy ```html ... ``` ### IntersectionObserver (커스텀) ```typescript const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { const img = e.target as HTMLImageElement; img.src = img.dataset.src!; io.unobserve(img); } }); }, { rootMargin: "200px" }); document.querySelectorAll("img[data-src]").forEach(img => io.observe(img)); ``` ### React.lazy + Suspense (route) ```tsx import { lazy, Suspense } from "react"; const Settings = lazy(() => import("./pages/Settings")); }> } /> ``` ### Dynamic import on interaction ```tsx function ChartButton() { const [Chart, setChart] = useState(null); return ( <> {Chart && } ); } ``` ### Virtual scrolling (TanStack Virtual) ```tsx import { useVirtualizer } from "@tanstack/react-virtual"; const v = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 48, overscan: 5, }); return v.getVirtualItems().map(vi => (
{items[vi.index].name}
)); ``` ### React Query infinite scroll ```tsx const q = useInfiniteQuery({ queryKey: ["posts"], queryFn: ({ pageParam = 0 }) => fetch(`/api?cursor=${pageParam}`).then(r => r.json()), getNextPageParam: (last) => last.nextCursor, }); ``` ### Idle-time prefetch ```typescript if ("requestIdleCallback" in window) { requestIdleCallback(() => import("./LikelyNextRoute")); } ``` ### Next.js dynamic ```tsx import dynamic from "next/dynamic"; const Map = dynamic(() => import("./Map"), { ssr: false, loading: () =>

...

}); ``` ## 매 결정 기준 | 자원 | 전략 | |---|---| | Above-the-fold image | eager + preload | | Below-the-fold image | `loading="lazy"` | | 큰 third-party (chart, map) | dynamic import on demand | | Route component | route-based code split | | 1만+ list rows | virtual scroll | | 다음에 갈 가능성 큰 route | idle prefetch | **기본값**: native `loading="lazy"` for img, `React.lazy` for routes, virtual scroll > 200 rows. ## 🔗 Graph - 부모: [[Web-Performance]] - 변형: [[Code Splitting]], [[Infinite-Scroll]] - Adjacent: [[IntersectionObserver]], [[Suspense]] ## 🤖 LLM 활용 **언제**: 페이지에 heavy component 가 conditional 로 필요, 큰 list, 이미지 많은 페이지. **언제 X**: above-the-fold critical content, SEO-critical content (SSR 으로 처리). ## ❌ 안티패턴 - **모든 것을 lazy**: critical hero image 까지 lazy → LCP 악화. - **width/height 누락**: lazy image 로딩 후 layout shift (CLS↑). - **Suspense fallback 부재**: blank flash. - **Interaction-block lazy**: 클릭하면 1초 fetch → ux 나쁨, idle prefetch 병행. - **Spinner 남발**: skeleton/blur placeholder 가 체감 좋음. ## 🧪 검증 / 중복 - web.dev "Lazy loading", MDN IntersectionObserver, React docs (lazy/Suspense). - TanStack Virtual, Next.js dynamic 공식 문서. - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — image/route/data 4 layer, IntersectionObserver/React.lazy/virtual 패턴 |