- Published on
Next.js LCP가 늦는 이유 - 이미지·폰트 최적화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 컴포넌트, 스트리밍, ISR 같은 Next.js 기능을 잘 쓰고 있는데도 Lighthouse에서 LCP(Largest Contentful Paint)가 계속 느리게 나오는 경우가 많습니다. 특히 랜딩 페이지나 블로그 글 상세에서 LCP 엘리먼트가 히어로 이미지 또는 헤딩 텍스트인 경우, 실제 병목은 대개 이미지 전송/디코딩/레이아웃 또는 폰트 로딩(FOIT/FOUT) 에서 발생합니다.
이 글에서는 Next.js 기준으로 LCP가 늦어지는 원인을 “관측 → 원인 분해 → 이미지 최적화 → 폰트 최적화 → 체크리스트” 순서로 정리합니다.
> 참고: 렌더링 불일치로 인해 초기 페인트가 꼬이는 경우도 LCP에 간접 영향을 줄 수 있습니다. 관련해서는 Next.js Hydration mismatch 원인 9가지와 해결법도 함께 확인해두면 좋습니다.
LCP가 늦는지 먼저 ‘어디서’ 늦는지 쪼개기
LCP는 단순히 “서버가 느리다/클라이언트가 느리다”로 끝나지 않습니다. Chrome DevTools와 Lighthouse는 LCP를 대략 다음 단계로 분해해 보여줍니다.
- TTFB: HTML 첫 바이트까지(서버/캐시/네트워크)
- Load Delay: LCP 리소스 요청이 늦게 시작됨(우선순위/프리로드/JS 차단)
- Load Time: 다운로드 자체가 오래 걸림(용량/압축/캐시)
- Render Delay: 다운로드 후 디코딩/레이아웃/폰트 적용이 늦음
빠른 진단 루틴
- DevTools → Performance 기록 후 Largest Contentful Paint 이벤트 클릭
- “LCP by phase”에서 Load Delay vs Load Time vs Render Delay 중 어디가 큰지 확인
- Network 탭에서 LCP 후보(대개 히어로 이미지, H1 텍스트)의 요청 시점/우선순위/캐시 히트를 확인
이 글의 핵심은 LCP가 이미지/폰트에서 느려지는 대표 패턴을 Next.js에서 어떻게 해결하는지입니다.
이미지가 LCP를 잡아먹는 대표 원인 6가지
히어로 이미지가 LCP인 페이지에서 가장 자주 보는 문제는 다음입니다.
- 원본이 너무 큼: 3000px JPEG를 모바일에 그대로 내려줌
- 포맷 비효율: WebP/AVIF 미사용
- 잘못된 사이즈 계산:
sizes미설정으로 과도한 리소스 선택 - 우선순위 낮음: LCP 이미지가 lazy 로딩이거나 preload가 없음
- 레이아웃 시프트: width/height 미지정으로 레이아웃 확정이 늦음
- 외부 이미지 도메인/리다이렉트: 추가 DNS/Handshake 비용
아래부터는 Next.js에서의 구체적 해결입니다.
Next.js 이미지 최적화: next/image를 ‘제대로’ 쓰기
Next.js의 next/image는 기본적으로 리사이징/최적화/지연 로딩을 제공하지만, LCP 관점에서 기본값만으로는 부족한 경우가 많습니다.
1) LCP 이미지에는 priority + fetchPriority를 명시
히어로 이미지가 LCP라면 lazy 로딩이 아니라 즉시 로딩되어야 합니다.
// app/(marketing)/page.tsx
import Image from "next/image";
export default function Page() {
return (
<main>
<section>
<Image
src="/images/hero.jpg"
alt="제품 히어로"
width={1600}
height={900}
priority
fetchPriority="high"
sizes="(max-width: 768px) 100vw, 1200px"
style={{ width: "100%", height: "auto" }}
/>
<h1>빠른 LCP를 위한 Next.js 최적화</h1>
</section>
</main>
);
}
priority: Next.js가 preload 힌트를 추가하고 lazy를 해제fetchPriority="high": 브라우저가 해당 리소스를 더 공격적으로 우선 다운로드(지원 브라우저에서 효과)width/height: 레이아웃 확정이 빨라져 Render Delay 감소
2) sizes를 반드시 지정해 과도한 다운로드를 막기
sizes가 없으면 브라우저/Next가 큰 이미지를 선택할 확률이 높아집니다. 모바일에서 1200px 이상을 내려받으면 LCP가 바로 늘어납니다.
<Image
src="/images/hero.jpg"
alt="hero"
fill
priority
sizes="(max-width: 480px) 100vw, (max-width: 1024px) 90vw, 1200px"
style={{ objectFit: "cover" }}
/>
fill을 쓰는 경우 부모 컨테이너에 고정된 aspect ratio를 주지 않으면 레이아웃 확정이 늦어질 수 있습니다.
3) 원격 이미지라면 next.config의 remotePatterns와 캐시 전략 점검
외부 CDN/스토리지에서 이미지를 가져오면 DNS/TLS 비용이 추가됩니다. 또한 Next 이미지 최적화 서버가 원격 이미지를 다시 가져오느라 지연될 수 있습니다.
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
pathname: "/assets/**",
},
],
},
};
권장:
- 가능하면 동일 CDN/도메인에서 서빙(연결 재사용)
- 원격 원본에 리다이렉트가 없는지 확인
- 이미지 최적화 결과가 캐시되는지(CDN, Vercel Edge 캐시 등) 확인
4) 포맷/품질: AVIF/WebP와 과도한 quality 방지
next/image는 기본적으로 브라우저 지원에 따라 WebP 등을 제공하지만, 원본이 지나치게 크면 한계가 있습니다.
- 히어로는 가로 1200~1600px 범위에서 충분한 경우가 많음
- 사진 계열은
quality60~75 선에서 체감 품질 유지
<Image
src="/images/hero.jpg"
alt="hero"
width={1600}
height={900}
priority
quality={70}
sizes="(max-width: 768px) 100vw, 1200px"
/>
5) blur placeholder는 상황에 따라 LCP를 악화시킬 수 있음
placeholder="blur"는 UX에 좋지만, LCP 측정 대상이 실제 이미지인 경우 blur → full 전환이 렌더 타이밍에 영향을 줄 수 있습니다.
- LCP 이미지에는 blur를 빼고, 아래 폴드 이미지에 blur를 적용하는 전략이 종종 유리합니다.
폰트가 LCP를 늦추는 대표 원인 5가지
LCP 엘리먼트가 텍스트(H1/헤드라인)인 페이지에서, 폰트가 병목이 되는 패턴은 다음입니다.
- FOIT(텍스트 숨김): 폰트 로딩 전까지 텍스트가 안 보임
- 서브셋 미사용: 한글 전체 글리프를 매번 내려받음
- 폰트 파일이 많음: weight 6개 + italic 등 과다
- 프리로드 누락: CSS 파싱 후에야 폰트 요청 시작
- CORS/캐시 미흡: 폰트가 캐시되지 않거나 매번 재검증
Next.js에서는 next/font를 사용하면 많은 문제를 구조적으로 줄일 수 있습니다.
Next.js 폰트 최적화: next/font로 preload + self-hosting
1) next/font/google로 자동 최적화(추천)
Google Fonts를 직접 <link>로 붙이면 CSS 로딩/폰트 로딩 순서가 꼬이기 쉽습니다. next/font/google는 최적화된 로딩과 preload를 제공합니다.
// app/layout.tsx
import "./globals.css";
import { Noto_Sans_KR } from "next/font/google";
const noto = Noto_Sans_KR({
subsets: ["latin"], // KR은 google 폰트에 따라 subsets 옵션이 제한될 수 있음
weight: ["400", "700"],
display: "swap",
preload: true,
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" className={noto.className}>
<body>{children}</body>
</html>
);
}
핵심 포인트:
display: "swap": FOIT 방지(텍스트가 먼저 보이고 폰트가 교체)- weight를 꼭 필요한 만큼만(대개 2개면 충분)
2) next/font/local로 로컬 폰트 self-host + 서브셋 적용
한글 폰트는 용량이 커서 LCP에 치명적입니다. 가능하면:
- 서브셋(필요 글자만)
- 가변 폰트(variable) 또는 weight 최소화
- woff2만 사용
// app/layout.tsx
import localFont from "next/font/local";
const pretendard = localFont({
src: [
{
path: "../public/fonts/Pretendard-Regular.subset.woff2",
weight: "400",
style: "normal",
},
{
path: "../public/fonts/Pretendard-Bold.subset.woff2",
weight: "700",
style: "normal",
},
],
display: "swap",
preload: true,
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" className={pretendard.className}>
<body>{children}</body>
</html>
);
}
서브셋 파일 생성은 pyftsubset(fonttools)나 glyphhanger 등을 사용합니다. 목표는 “첫 화면에 필요한 글리프만 포함한 woff2”로 수십~수백 KB 수준으로 낮추는 것입니다.
3) 폰트 프리로드를 직접 제어해야 하는 경우
next/font를 쓰기 어려운 레거시 구조라면, 최소한 LCP에 영향을 주는 폰트는 preload를 고려합니다.
// app/head.tsx (또는 layout에서 metadata/Head 사용)
export default function Head() {
return (
<>
<link
rel="preload"
href="/fonts/Pretendard-Regular.subset.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</>
);
}
주의:
- preload 남발은 오히려 네트워크 경합을 만들 수 있어 핵심 폰트 1~2개만
이미지·폰트 외에 LCP를 악화시키는 Next.js 특유의 함정
1) LCP 리소스가 ‘클라이언트 컴포넌트’ 뒤에 숨어 있는 경우
히어로 섹션이 use client 컴포넌트에 있고, 그 안에서 상태/이펙트로 렌더가 지연되면 LCP가 늦어질 수 있습니다.
- 가능하면 히어로(특히 LCP 이미지/헤딩)는 서버 컴포넌트로 두고
- 상호작용이 필요한 부분만 클라이언트로 분리합니다.
2) 과도한 JS가 메인 스레드를 점유(Render Delay 증가)
이미지 다운로드는 끝났는데도 LCP가 늦다면, JS 실행/스타일 계산/레이아웃이 병목일 수 있습니다.
- 불필요한 3rd-party 스크립트(태그매니저, A/B, 채팅) 지연 로딩
next/script의strategy활용
import Script from "next/script";
export default function Page() {
return (
<>
<Script src="https://example.com/heavy.js" strategy="lazyOnload" />
{/* ... */}
</>
);
}
실전 체크리스트: LCP를 빠르게 만드는 우선순위
아래 순서대로 적용하면 체감 개선이 빠릅니다.
- LCP 엘리먼트가 무엇인지 먼저 확정(이미지인지 텍스트인지)
- 이미지가 LCP라면
priority,fetchPriority="high"sizes정확히 설정- 원본 리사이즈/압축(가로 1200~1600px 목표)
- 외부 도메인/리다이렉트 제거, CDN 캐시 확인
- 텍스트가 LCP라면
next/font로 전환display: swap- weight 최소화 + woff2 + 서브셋
- Render Delay가 크다면
- LCP 영역을 서버 컴포넌트로
- 3rd-party 스크립트 지연 로딩
마무리: “LCP는 결국 가장 큰 것의 가장 빠른 도착”
Next.js에서 LCP 최적화는 거창한 기법보다, LCP 후보(대개 히어로 이미지/헤딩 텍스트) 의 로딩 우선순위를 올리고(프리로드/priority), 전송량을 줄이고(리사이즈/서브셋), 렌더를 방해하는 요소(JS/레이아웃 불안정)를 제거하는 싸움입니다.
특히 이미지와 폰트는 한 번만 제대로 세팅해도 전체 페이지에 파급 효과가 큽니다. 위의 체크리스트대로 적용한 뒤 Lighthouse의 LCP phase를 다시 확인하면, 어디가 줄었고 어디가 남았는지 명확하게 보일 것입니다.