- Published on
Next.js 이미지 LCP 느림? AVIF·sizes·priority 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 컴포넌트와 스트리밍이 기본이 된 Next.js 환경에서도, 실제 사용자 체감 속도를 가장 크게 좌우하는 건 여전히 LCP(Largest Contentful Paint) 입니다. 특히 히어로 이미지나 카드 썸네일처럼 화면 상단에 큰 이미지가 있는 페이지는, 이미지 한 장이 LCP 후보가 되면서 점수가 급격히 나빠집니다.
이 글은 Next.js의 next/image를 기준으로 AVIF, sizes, **priority**를 중심으로 LCP를 개선하는 실전 체크리스트를 제공합니다. 단순히 "압축률 좋은 포맷 쓰세요"가 아니라, 브라우저가 어떤 리소스를 선택하고 언제 다운로드를 시작하는지까지 포함해 설명합니다.
LCP가 이미지에서 느려지는 대표 원인
1) sizes가 없거나 틀려서 과대 용량을 받는 경우
next/image는 내부적으로 srcset을 생성하고, 브라우저는 sizes를 참고해 "이 이미지가 뷰포트에서 어느 정도 크기로 렌더링되는지"를 추정한 뒤 적절한 후보를 고릅니다.
문제는 sizes가 없으면(또는 100vw로 뭉뚱그리면) 브라우저가 가장 큰 후보를 선택하는 일이 흔하다는 점입니다. 결과적으로 모바일에서도 데스크톱급 이미지를 받아 LCP가 느려집니다.
2) LCP 후보 이미지가 늦게 발견되는 경우
이미지가 DOM 아래쪽에 있거나, 레이아웃 시프트를 막기 위해 CSS로 숨겼다가 보여주거나, 클라이언트에서 조건부 렌더링을 하면 브라우저가 이미지 요청을 늦게 시작합니다.
특히 히어로 영역 이미지는 초기 HTML에서 바로 노출되고 바로 요청되어야 합니다.
3) priority를 남발해서 네트워크가 경합하는 경우
priority는 LCP 후보(대개 첫 화면 히어로 이미지) 같은 소수의 리소스에만 써야 합니다. 리스트의 썸네일, 아래쪽 섹션 이미지까지 priority를 주면 오히려 중요한 이미지가 먼저 내려받지 못해 LCP가 악화됩니다.
4) 포맷/압축은 좋아졌는데 디코딩이 무거운 경우
AVIF는 대체로 용량을 크게 줄여주지만, 일부 환경에서 디코딩 비용이 커질 수 있습니다. 즉, "네트워크 전송"만 줄이고 "디코딩"에서 시간을 잃을 여지가 있습니다.
그래서 AVIF는 무조건이 아니라, LCP 이미지에 적용했을 때 실제 사용자 지표가 개선되는지를 확인해야 합니다.
진단: LCP가 정말 이미지인지부터 확인
Chrome DevTools의 Performance 또는 Lighthouse에서 LCP 요소가 무엇인지 확인하세요.
- LCP 요소가
img혹은next/image로 렌더링된 이미지인지 - LCP 타이밍에서 병목이
TTFB,Load Delay,Load Time,Render Delay중 어디인지
여기서 Load Delay가 크면 “이미지 요청 시작이 늦음”, Load Time이 크면 “용량/전송/캐시 문제”, Render Delay가 크면 “디코딩/메인스레드/스타일 계산” 가능성이 큽니다.
프론트 성능 이슈를 더 넓게 다루는 글이 필요하면 Chrome INP 점수 급락? Long Task 추적·해결도 함께 참고하면 좋습니다.
next/image로 LCP 최적화하는 핵심 3가지
1) AVIF 적용: "가능하면"이 아니라 "검증하며" 적용
Next.js는 기본적으로 이미지 최적화 파이프라인을 제공하고, 브라우저가 지원하면 더 효율적인 포맷을 내려줄 수 있습니다. 다만 환경에 따라 설정 방식이 조금씩 다를 수 있으니, 핵심은 아래 두 가지입니다.
- LCP 후보 이미지는 AVIF로 전송되어 바이트를 줄이는지
- AVIF 디코딩 때문에
Render Delay가 늘지 않는지
체크 포인트
- LCP 이미지의 전송 크기(KB)가 유의미하게 감소하는가
- 모바일 실기기에서 스크롤 없이 첫 화면 로딩 체감이 개선되는가
예시: next/image 사용 (히어로)
import Image from 'next/image'
export default function Hero() {
return (
<section>
<Image
src="/images/hero.jpg"
alt="Product hero"
width={1600}
height={900}
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1200px"
/>
</section>
)
}
포인트는 AVIF를 코드에서 직접 지정하는 게 아니라, Next의 이미지 최적화가 적절한 포맷을 선택하도록 두되, 네트워크 패널에서 실제로 image/avif로 내려오는지 확인하는 것입니다.
만약 외부 이미지 호스트를 쓰는 경우, CDN이 AVIF 변환을 제공하는지(또는 원본이 이미 AVIF인지)도 함께 점검하세요.
2) sizes는 "정확한 렌더 폭"을 적는 게 성능의 절반
sizes는 브라우저의 이미지 후보 선택에 직접적인 힌트를 줍니다.
100vw만 쓰면: 데스크톱에서 실제로는 1200px만 쓰는데도 큰 후보를 고를 수 있음- 브레이크포인트별로 "실제 렌더 폭"을 써주면: 불필요하게 큰 파일을 받지 않음
흔한 실수: 카드 썸네일인데 sizes="100vw"
리스트 카드 썸네일이 실제로는 모바일에서 화면의 절반, 데스크톱에서 300px 정도만 차지하는데 100vw로 두면 과대 다운로드가 발생합니다.
예시: 그리드 카드 이미지
import Image from 'next/image'
export function CardThumb({ src, title }: { src: string; title: string }) {
return (
<Image
src={src}
alt={title}
width={600}
height={400}
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 300px"
/>
)
}
이렇게 하면 브라우저가 "모바일에서는 절반 폭", "태블릿에서는 3열", "데스크톱에서는 300px"로 추정해 더 작은 후보를 선택할 가능성이 커집니다.
fill을 쓸 때의 sizes
fill은 레이아웃에 따라 이미지가 늘어나므로 sizes가 더 중요합니다.
<div style={{ position: 'relative', width: '100%', height: 360 }}>
<Image
src="/images/hero.jpg"
alt="Hero"
fill
priority
sizes="(max-width: 768px) 100vw, 1200px"
style={{ objectFit: 'cover' }}
/>
</div>
fill을 쓰면서 sizes를 빼먹으면, 브라우저는 보수적으로 큰 리소스를 선택하는 쪽으로 기웁니다.
3) priority는 LCP 후보 1장(많아도 2장)만
priority는 사실상 "이 이미지는 초기 로딩에서 가장 중요하니 먼저 가져와"라는 강한 힌트입니다.
priority를 써야 하는 경우
- 첫 화면 히어로 이미지
- LCP 후보로 확정된 대표 이미지
priority를 피해야 하는 경우
- 리스트 썸네일 전체
- 스크롤해야 보이는 섹션
- 모달/탭 내부 이미지
실전 패턴: LCP 이미지에만 priority, 나머지는 지연
import Image from 'next/image'
export default function Page() {
return (
<main>
<Image
src="/images/hero.jpg"
alt="Hero"
width={1600}
height={900}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
{/* 아래 이미지는 priority를 주지 않는다 */}
<section>
<Image
src="/images/detail-1.jpg"
alt="Detail"
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, 800px"
/>
</section>
</main>
)
}
이렇게 하면 네트워크 경합을 줄이고, LCP 후보 리소스가 더 빨리 내려받아질 확률이 높아집니다.
추가 최적화 체크리스트 (LCP를 더 안정화)
1) LCP 이미지의 레이아웃을 고정해서 CLS와 경합 줄이기
width/height 또는 fill 기반의 고정 컨테이너를 사용해 레이아웃 점프를 막아야 합니다. CLS가 커지면 렌더링 파이프라인이 흔들리면서 LCP도 악영향을 받을 수 있습니다.
2) 서버에서 LCP 이미지가 빨리 발견되도록 마크업 단순화
- 히어로 이미지를 조건부 렌더링으로 숨기지 않기
- 클라이언트 상태가 준비된 뒤에야 이미지를 렌더링하는 패턴 피하기
- 가능하면 상단 섹션은 서버 렌더로 즉시 노출
3) CDN 캐시와 이미지 최적화 캐시 확인
Next 이미지 최적화는 캐시가 중요합니다.
- 동일 URL에 대해 변환된 결과가 캐시되는지
- 배포/무효화 정책 때문에 매 요청마다 변환이 재발생하지 않는지
인프라 레벨에서 병목이 생기면 프론트에서 아무리 sizes를 다듬어도 한계가 있습니다. 운영 환경에서 배포/캐시 문제가 성능으로 번지는 사례는 Argo CD Image Updater 미동작 원인 7가지 같은 글의 관점(원인 분해, 관측, 재현)으로 접근하면 해결이 빨라집니다.
4) LCP 이미지 용량의 "상한"을 정해두기
경험적으로 히어로 이미지가 LCP를 잡는 페이지라면, 모바일 기준 전송 크기를 대략 아래처럼 관리하면 안정적입니다.
- 히어로(풀폭): 수십 KB에서 150KB 내외를 목표
- 카드 썸네일: 한 장당 10KB에서 40KB 수준을 목표
정답은 아니지만, 상한이 없으면 팀이 커질수록 이미지가 점점 무거워지는 방향으로 drift가 발생합니다.
트러블슈팅: 자주 겪는 함정
1) sizes는 맞는데도 큰 이미지를 받는다
- 실제 CSS 레이아웃이
sizes가정과 다를 수 있습니다. - 특히
max-width,container,gap을 고려하지 않으면 계산이 어긋납니다.
해결은 “정확한 픽셀 폭을 맞추는 것”보다, 브레이크포인트별로 보수적인 상한(px)을 명시하는 방식이 실용적입니다.
2) AVIF로 바꿨는데 LCP가 오히려 나빠졌다
가능한 원인:
- 디코딩 비용 증가로
Render Delay가 늘어남 - 네트워크는 빨라졌지만 메인 스레드가 바쁜 상태(긴 태스크)라 그리기가 늦음
이 경우 AVIF를 LCP 이미지에만 제한하거나, 해당 이미지만 WebP로 유지하는 식의 타협이 필요할 수 있습니다. 메인 스레드 병목은 INP/Long Task 관점에서 같이 봐야 하므로 위에서 언급한 INP 글을 함께 보는 게 좋습니다.
3) priority를 줬는데도 요청이 늦다
- LCP 이미지가 실제로는 상단이 아닌 컴포넌트 트리 깊은 곳에서 늦게 렌더링될 수 있습니다.
- 또는 폰트/스크립트가 먼저 네트워크를 점유하고 있을 수 있습니다.
이때는 상단 구조를 단순화하고, 정말 필요한 리소스만 초기 로드에 남기는 게 핵심입니다.
정리: LCP 이미지 최적화는 "선택 정확도" 싸움
Next.js에서 이미지 LCP가 느릴 때 가장 효과적인 순서는 보통 아래와 같습니다.
- LCP 후보가 어떤 이미지인지 정확히 식별
priority는 LCP 후보 1장에만 적용sizes를 레이아웃에 맞게 작성해서 과대 다운로드 방지- AVIF는 적용하되, 전송 이득과 디코딩 비용을 실제 지표로 검증
이미지 최적화는 한 번 설정하고 끝이 아니라, 레이아웃이 바뀌거나 디자인이 바뀔 때마다 sizes가 틀어지기 쉽습니다. 릴리즈마다 LCP 후보 이미지와 전송 크기, 그리고 sizes의 적합성을 점검하는 루틴을 만들어두면 Core Web Vitals가 장기적으로 안정화됩니다.