본 문서는 기본 개념부터 실전 코드, 심화 이론까지 한 번에 정리한 확장판입니다.
아래에는 LCP·CLS 시각화 그림과 즉시 적용 가능한 최적화 코드 예제가 포함되어 있습니다.
아래 타임라인은 탐색 시작 → FCP → LCP 이벤트의 상대적 시점을 보여줍니다.
개선 포인트
점선 박스는 레이아웃이 이동된 최종 위치를 의미합니다.
개선 포인트
aspect-ratio
지정transform
/opacity
사용HTML: 고정 크기와 지연 로딩
<!-- 히어로 이미지: width/height 또는 aspect-ratio 지정 + lazy는 fold 아래에서만 -->
<img
src="/images/hero.webp"
alt="Hero"
width="1200"
height="630"
decoding="async"
fetchpriority="high" <!-- Above-the-fold일 때만 -->
/>
<!-- 아래 영역 이미지 -->
<img
src="/images/gallery-1.avif"
alt="Gallery"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
CSS: 비율 기반 자리 확보
.card-media {
aspect-ratio: 4 / 3; /* 고정 크기가 어려울 때 CLS 방지 */
object-fit: cover;
}
Video: 포스터 + 미리 연결
<link rel="preconnect" href="https://cdn.example.com">
<video src="https://cdn.example.com/intro.webm" preload="metadata" poster="/images/intro-poster.jpg" playsinline muted></video>
CSS: font-display로 FOIT/FOUT 제어
@font-face {
font-family: "InterVar";
src: url("/fonts/Inter-Variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap; /* 빠른 텍스트 표시 */
}
Metrics Override (CLS 감소: 폰트 교체 시 reflow 축소)
@font-face {
font-family: "Brand Sans";
src: url("/fonts/BrandSans.woff2") format("woff2");
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 102%;
font-display: optional;
}
스크립트 로딩 전략
<!-- 크리티컬하지 않은 스크립트는 defer/async -->
<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" type="module"></script> <!-- 모듈은 기본 defer와 유사 -->
긴 태스크 분할 (메인 스레드 점유 단축)
function chunkedWork(items, process, chunk = 100) {
let i = 0;
function step(deadline) {
while (i < items.length && (deadline.timeRemaining() > 0 || deadline.didTimeout)) {
for (let c = 0; c < chunk && i < items.length; c++, i++) {
process(items[i]);
}
}
if (i < items.length) requestIdleCallback(step, { timeout: 200 });
}
requestIdleCallback(step);
}
// 사용
chunkedWork(bigArray, heavyCompute);
이벤트 핸들러 최적화 (INP 개선)
// 가벼운 핸들러에서 상태만 큐잉하고, 실제 무거운 작업은 idle/timeout으로 미룸
button.addEventListener('click', () => {
queueMicrotask(() => {
startTransition(); // React 18이라면 사용자 입력 우선권 보장
});
});
<!-- LCP에 필요한 히어로 이미지, 핵심 폰트 등을 preload -->
<link rel="preload" as="image" href="/images/hero.webp" imagesrcset="/images/hero@2x.webp 2x" fetchpriority="high">
<link rel="preload" as="font" href="/fonts/Inter-Variable.woff2" type="font/woff2" crossorigin>
<!-- 다음 페이지 리소스는 prefetch로 힌트 -->
<link rel="prefetch" href="/next/chunk-abc123.js" as="script">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
// Next.js 13+
// 1) dynamic import로 코드 스플리팅
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('./HeavyChart'), { ssr: false })
// 2) 이미지 최적화
import Image from 'next/image'
export default function Hero() {
return (
<>
<Image
src="/images/hero.webp"
alt="Hero"
priority // LCP 후보일 때
width={1200}
height={630}
/>
<HeavyChart />
</>
)
}
// React 18 concurrent 특성 활용 (INP 개선)
import { startTransition } from 'react'
function onFilterChange(next) {
startTransition(() => {
// 무거운 상태 변경은 낮은 우선순위로
setFilter(next)
})
}
<script type="module">
import { onCLS, onINP, onLCP } from 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.js'
function sendToAnalytics(metric) {
// fetch/beacon 등으로 서버에 전송
console.log(metric.name, metric.value, metric.attribution)
}
onCLS(sendToAnalytics)
onINP(sendToAnalytics)
onLCP(sendToAnalytics)
</script>