- Published on
Next.js 이미지 LCP 폭증? priority·sizes 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 API가 빠른데도 Lighthouse/CrUX에서 LCP가 갑자기 폭증했다면, 범인은 의외로 next/image 설정인 경우가 많습니다. 특히 priority를 무분별하게 켜거나, sizes를 비워 둔 채 반응형 레이아웃을 쓰면 브라우저가 “가장 큰 이미지”를 더 큰 리소스로 판단해 LCP 후보가 커지고, 네트워크/디코드 비용까지 함께 증가합니다.
이 글은 Next.js(주로 App Router 기준)에서 이미지로 인한 LCP 악화를 재현 가능한 형태로 분해하고, priority와 sizes를 중심으로 “다운로드되는 바이트 자체를 줄이는” 방향으로 최적화하는 방법을 다룹니다.
LCP가 이미지에서 폭증하는 전형적인 패턴
LCP(Largest Contentful Paint)는 “뷰포트 내에서 가장 큰 콘텐츠 요소(텍스트 블록 또는 이미지)가 그려진 시점”입니다. 히어로 이미지가 화면에서 가장 큰 경우가 많아 LCP 후보가 이미지로 잡히는 건 정상입니다. 문제는 아래와 같은 이유로 LCP 후보 이미지가 불필요하게 무거워지는 것입니다.
1) priority 남용으로 초기 네트워크가 혼잡해짐
priority는 내부적으로 preload 힌트를 주고, 초기 로딩에서 해당 이미지를 우선순위 높게 당겨옵니다. 히어로 1장에만 써야 하는데, 카드 썸네일·섹션 배너 등 여러 장에 붙이면 초기 요청이 과밀해져 진짜 LCP 이미지가 필요한 대역폭을 못 받는 상황이 생깁니다.
2) sizes 미설정으로 과도한 해상도가 선택됨
반응형 레이아웃에서 fill 또는 큰 width를 쓰면서 sizes를 지정하지 않으면, 브라우저는 보수적으로 판단해 큰 리소스를 선택하거나, Next.js가 생성한 srcset 중 불필요하게 큰 후보를 선택할 수 있습니다. 결과적으로 “실제 화면에서 360px로 보이는 이미지”를 “1000px 이상”으로 내려받아 LCP가 늘어납니다.
3) CSS로 줄여 보이게 만든 큰 이미지
예: 원본은 1200x800인데 CSS로 max-width: 320px처럼 줄여 표시하는 경우. 브라우저는 표시 크기와 다운로드 크기가 다르면 더 큰 쪽을 택하거나, 디코드 비용이 늘어납니다.
4) LCP 이미지가 늦게 렌더링되는 구조
- LCP 이미지가 스켈레톤/모달/탭 뒤에서 늦게 나타남
- 서버 컴포넌트에서 데이터 대기 후에야 이미지가 렌더됨
- 클라이언트 전용 컴포넌트에서 hydration 이후에야 이미지가 나타남
이 경우 priority만으로는 해결이 안 되고, 렌더링 시점 자체를 앞당기거나 레이아웃을 바꿔야 합니다.
10분 진단 체크리스트 (실무용)
1) Chrome DevTools에서 LCP 후보 확인
- DevTools
Performance탭 기록 Timings에서 LCP 이벤트 클릭Related Node로 어떤 요소가 LCP인지 확인
이미지라면, 해당 이미지가 어떤 URL로 내려왔는지(리사이즈된 최종 /_next/image?... 포함), 전송 크기, 디코드 시간을 봅니다.
2) Network에서 preload/우선순위 확인
priority가 켜진 이미지는 preload 힌트가 생길 수 있습니다.- 초기 요청에 이미지가 너무 많지 않은지 확인합니다.
3) “화면에서 실제로 몇 px로 보이는가” 측정
Elements에서 해당 이미지의 렌더링 박스 크기를 확인하고, 그 크기에 맞는 sizes를 설계합니다.
추가로, 사용자 경험 지표를 함께 최적화하려면 INP도 같이 보세요. 이미지가 많을 때 메인 스레드가 디코드/레이아웃으로 바빠져 상호작용이 밀리는 케이스가 있습니다: Chrome INP 낮추기 - Long Task 원인추적·해결
priority 최적화: “딱 1장(또는 2장)만”
원칙은 단순합니다.
- 첫 화면(Above the fold)에서 LCP 후보가 되는 히어로 이미지 1장:
priority사용 - 그 외 이미지는 기본값(
loading="lazy")에 맡기거나, 정말 필요하면loading="eager"를 제한적으로 사용
나쁜 예: 리스트 썸네일까지 모두 priority
import Image from "next/image";
export function BadGallery({ items }: { items: { src: string; title: string }[] }) {
return (
<div className="grid">
{items.map((it) => (
<div key={it.src} className="card">
<Image
src={it.src}
alt={it.title}
width={600}
height={400}
priority
/>
</div>
))}
</div>
);
}
이 경우 초기 네트워크가 이미지로 가득 차서 정작 히어로가 늦어질 수 있습니다.
좋은 예: 히어로 1장만 priority
import Image from "next/image";
export function Hero() {
return (
<section className="hero">
<Image
src="/images/hero.jpg"
alt="메인 히어로"
fill
priority
sizes="(max-width: 768px) 100vw, 1200px"
style={{ objectFit: "cover" }}
/>
<h1>제품 소개</h1>
</section>
);
}
핵심은 priority 자체가 아니라, LCP 후보를 확정하고 그 리소스만 앞당기는 것입니다.
sizes 최적화: LCP 바이트를 직접 줄이는 핵심
next/image는 srcset(여러 폭의 이미지 후보)을 제공하고, 브라우저는 sizes를 보고 “이 이미지가 화면에서 어느 정도 크기로 렌더링될지”를 추정해 가장 적합한 후보를 고릅니다.
즉 sizes가 부정확하면 브라우저는 더 큰 후보를 선택할 수 있고, 그게 그대로 LCP 증가로 이어집니다.
1) fill을 쓸 때는 sizes가 사실상 필수
fill은 레이아웃에 따라 표시 크기가 달라지므로 sizes 없이 쓰면 최적 선택이 어려워집니다.
<Image
src="/images/hero.jpg"
alt="hero"
fill
priority
sizes="100vw"
style={{ objectFit: "cover" }}
/>
- 모바일/데스크톱 모두 화면 가로를 꽉 채우는 히어로라면
sizes="100vw"가 합리적입니다. - 데스크톱에서 최대 폭이 제한된다면, 그 제한값을 반영해야 합니다.
2) 컨테이너 최대 폭이 있는 경우
예를 들어 데스크톱에서 콘텐츠 영역이 최대 1024px이고, 모바일에서는 100vw라면:
<Image
src="/images/hero.jpg"
alt="hero"
fill
priority
sizes="(max-width: 1024px) 100vw, 1024px"
style={{ objectFit: "cover" }}
/>
이 한 줄이 “데스크톱에서 1600px짜리 내려받기” 같은 낭비를 크게 줄입니다.
3) 그리드/카드 썸네일 sizes
예: 모바일 2열, 태블릿 3열, 데스크톱 4열 그리드에서 카드 간 여백이 있다고 가정하면:
<Image
src={item.thumb}
alt={item.title}
width={400}
height={300}
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
이렇게 지정하면 브라우저가 각 뷰포트에서 “대략 몇 vw로 보이는지”를 근거로 더 작은 후보를 선택합니다.
LCP 관점에서 자주 하는 실수 5가지
1) LCP 이미지에 placeholder="blur"를 과도하게 기대
블러 플레이스홀더는 체감 개선에 도움 되지만, LCP 자체는 “실제 큰 콘텐츠가 그려진 시점”이라서 원본이 늦으면 LCP는 여전히 늦습니다. 블러는 보조 수단이고, 핵심은 전송 바이트/우선순위/렌더링 시점입니다.
2) 히어로 이미지를 클라이언트 컴포넌트에서만 렌더
히어로를 use client 컴포넌트로 만들고, 상태/이펙트 이후에 이미지가 나타나면 LCP가 밀립니다. 가능하면 서버 컴포넌트에서 바로 렌더하고, 상호작용만 클라이언트로 분리합니다.
3) quality를 기본값으로 두고 원본이 너무 큼
Next.js 이미지 최적화는 편하지만, 원본이 지나치게 크면 변환 비용도 커지고 네트워크도 커집니다. 히어로가 사진이라면 quality를 낮추는 것만으로도 LCP가 크게 개선될 수 있습니다.
<Image
src="/images/hero.jpg"
alt="hero"
fill
priority
quality={70}
sizes="(max-width: 1024px) 100vw, 1024px"
/>
4) 애니메이션/트랜지션으로 LCP 요소가 늦게 “완성”됨
opacity 페이드인 자체가 LCP를 늦추진 않더라도, 레이아웃 변경이나 큰 페인트를 유발하면 간접적으로 악화될 수 있습니다. 특히 큰 배경 이미지 위에서 복잡한 애니메이션을 하면 페인트 비용이 늘어납니다.
5) 이미지 최적화 서버가 병목
next/image는 런타임에서 리사이즈/변환을 수행할 수 있습니다. 트래픽이 몰릴 때 이미지 변환이 느려지면 LCP가 튑니다.
- 서버리스/컨테이너 환경에서 CPU 부족
- 콜드스타트로 이미지 변환이 늦어짐
런타임 지연이 의심되면 인프라 레벨도 같이 점검하세요. 예를 들어 콜드스타트나 워커 준비 지연은 LCP와 함께 TTFB에도 영향을 줍니다: GCP Cloud Run 503·콜드스타트 10분 지연 진단
실전 패턴: “히어로는 priority + 정확한 sizes, 나머지는 lazy + 정확한 sizes”
아래는 흔한 랜딩 페이지 구성 예시입니다.
- 히어로:
priority,fill, 명확한sizes - 섹션 이미지/썸네일:
loading기본(lazy),sizes로 다운로드 절감
import Image from "next/image";
export default function Page() {
return (
<main>
<section style={{ position: "relative", height: 420 }}>
<Image
src="/images/hero.jpg"
alt="hero"
fill
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1080px"
quality={70}
style={{ objectFit: "cover" }}
/>
<h1>Next.js 이미지 LCP 최적화</h1>
</section>
<section className="grid">
{Array.from({ length: 8 }).map((_, i) => (
<article key={i} className="card">
<Image
src={`/images/thumb-${i + 1}.jpg`}
alt={`thumb ${i + 1}`}
width={480}
height={320}
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
<h2>카드 {i + 1}</h2>
</article>
))}
</section>
</main>
);
}
이 구성의 목표는 명확합니다.
- LCP 후보(히어로)만 빠르게
- 나머지는 “필요해질 때” 적정 크기로
측정과 검증: 개선이 진짜인지 확인하는 방법
- Lighthouse는 실험실 환경이라 변동이 큽니다. 가능하면 CrUX 또는 RUM(실사용자 측정) 지표로 확인합니다.
- DevTools
Network에서 LCP 이미지의 전송 바이트가 줄었는지 확인합니다. Performance에서 LCP 타임라인이 앞당겨졌는지 확인합니다.- 이미지가 많은 페이지라면 메인 스레드에서
Decode Image가 길게 잡히지 않는지도 봅니다. 디코드가 길면 INP에도 악영향이 갈 수 있습니다.
결론: LCP 폭증의 80%는 priority가 아니라 sizes
priority는 “정말 중요한 1장”에만 쓰면 강력합니다. 하지만 LCP를 안정적으로 낮추는 핵심은 대부분 sizes로 다운로드되는 이미지 후보를 정확히 줄이는 것입니다.
정리하면 다음 순서로 접근하세요.
- LCP 후보 요소를 DevTools로 확정
- 히어로 1장에만
priority fill또는 반응형 이미지에는 반드시sizes지정- 데스크톱 최대 폭/그리드 열 수를 반영해
sizes를 현실적으로 작성 - 런타임 변환/콜드스타트 등 인프라 병목이 있으면 함께 제거
이렇게만 해도 “특정 배포 이후 LCP가 갑자기 2배로 튀는” 류의 이슈는 대부분 재발을 막을 수 있습니다.