- Published on
Chrome LCP 느림? Layout Shift 원인 6가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 빠른데도 Chrome에서 LCP가 늦게 찍히고, 화면이 ‘툭’ 밀리는 Layout Shift가 반복된다면 문제는 대개 렌더링 파이프라인과 레이아웃 안정성에 있습니다. 특히 LCP는 “가장 큰 콘텐츠가 실제로 그려진 시점”을 측정하므로, 폰트 로딩이나 이미지 디코딩, 메인 스레드 블로킹, 동적 삽입 같은 요소가 조금만 꼬여도 수백 ms에서 수 초까지 쉽게 밀립니다.
이 글에서는 실무에서 가장 자주 보는 Layout Shift 원인 6가지를, LCP와 어떤 식으로 엮여 느려지는지까지 함께 정리합니다. 마지막에는 DevTools로 원인을 ‘증거 기반’으로 좁히는 체크리스트도 제공합니다.
LCP와 CLS를 같이 봐야 하는 이유
LCP가 느린 페이지는 종종 CLS도 높습니다. 이유는 간단합니다.
- LCP 후보(히어로 이미지, H1, 큰 카드)가 늦게 로드되거나 나중에 크기가 확정되면, 그 순간 레이아웃이 재계산되며 주변 요소가 밀립니다.
- 레이아웃이 흔들리면 사용자는 “느리다”고 더 강하게 체감하고, 클릭 미스도 늘어납니다.
즉, LCP 최적화는 네트워크만 빠르게 한다고 끝나지 않고, “초기 레이아웃을 얼마나 확정적으로 잡아두느냐”가 핵심입니다.
측정 준비: DevTools에서 무엇을 봐야 하나
원인 규명은 감이 아니라 로그와 트레이스로 해야 합니다.
1) Performance 패널에서 LCP 이벤트 확인
- Chrome DevTools
Performance탭 Web Vitals체크 후 기록- 타임라인에서
LCP마커 클릭 - 어떤 요소가 LCP였는지(이미지, 텍스트 블록 등) 확인
2) Layout Shift 트랙과 “Cumulative Layout Shift” 확인
Experience섹션의 Layout Shift 이벤트를 클릭하면, 어떤 요소가 얼마나 이동했는지 오버레이로 보여줍니다.
3) Lighthouse는 재현성 낮을 수 있음
Lighthouse는 참고용으로 보고, 실제 원인 추적은 Performance 트레이스와 RUM(필드 데이터)로 보강하는 편이 좋습니다.
원인 1) 이미지/비디오 크기 미지정: 공간 예약 실패
가장 흔하고, 고치기도 쉬운 원인입니다.
img또는video에width/height가 없거나- CSS로만 크기를 주면서 원본 비율을 초기에 알 수 없거나
- 반응형 이미지에서 컨테이너 높이가 콘텐츠 로딩 후에야 결정되는 경우
이러면 브라우저는 초기 레이아웃에서 해당 영역 높이를 0에 가깝게 잡고, 리소스가 로드된 뒤에야 높이를 확정합니다. 그 순간 아래 콘텐츠가 밀리며 CLS가 발생하고, LCP 후보가 이미지라면 LCP도 늦게 찍힙니다.
해결
- 가능하면
width/height로 고정 비율을 제공 - 또는
aspect-ratio로 공간 예약
<img
src="/hero.jpg"
width="1200"
height="630"
alt="Hero"
loading="eager"
fetchpriority="high"
/>
.hero {
width: 100%;
aspect-ratio: 1200 / 630;
object-fit: cover;
}
추가로, 히어로 이미지가 LCP 후보라면 loading="lazy"는 피하고, fetchpriority="high"를 고려하세요.
원인 2) 웹폰트 로딩 정책 문제: FOIT/FOUT로 레이아웃 흔들림
웹폰트는 텍스트 폭과 줄바꿈을 바꾸기 때문에 CLS의 단골입니다.
- FOIT: 폰트 로딩 전 텍스트를 숨겼다가 나타남
- FOUT: 시스템 폰트로 먼저 그렸다가 웹폰트로 바뀌며 폭이 달라짐
둘 다 레이아웃 변화를 유발할 수 있고, 특히 LCP가 큰 텍스트 블록일 때 폰트 준비가 늦으면 LCP도 함께 지연됩니다.
해결
font-display: swap또는 상황에 따라optional- 핵심 폰트는
preload - 폴백 폰트를 metric 호환되게 선택
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
}
body {
font-family: "MyFont", system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", sans-serif;
}
<link
rel="preload"
href="/fonts/myfont.woff2"
as="font"
type="font/woff2"
crossorigin
/>
실무 팁으로, 폰트 자체 최적화보다 “히어로 영역에 쓰는 폰트만 우선 준비”하는 전략이 LCP에 더 직접적입니다.
원인 3) 상단 배너/공지/쿠키 바를 늦게 삽입: DOM 삽입으로 밀어내기
페이지가 그려진 뒤에 상단에 배너를 prepend하거나, 쿠키 동의 바를 상단에 끼워 넣으면 아래 콘텐츠가 통째로 내려가 CLS가 크게 튑니다. 특히 사용자 상호작용 없이 발생한 shift는 평가가 더 나쁩니다.
해결
- 처음부터 “자리”를 예약하거나
- 오버레이로 띄워 레이아웃을 밀지 않게 처리
.top-banner-slot {
height: 56px; /* 초기부터 공간 예약 */
}
.top-banner {
height: 56px;
}
또는 레이아웃을 밀지 않도록:
.cookie-banner {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
}
body {
padding-bottom: 0; /* 필요 시 안전영역만 별도 처리 */
}
핵심은 “나중에 DOM이 들어오더라도 레이아웃이 변하지 않게” 만드는 것입니다.
원인 4) 광고/임베드/서드파티 위젯: 크기 확정이 늦고 예측 불가
광고, 댓글 위젯, 추천 위젯, 소셜 임베드는 다음 특징 때문에 CLS와 LCP를 동시에 망가뜨립니다.
- 로드 시점이 늦다
- 내부에서 추가 리소스를 연쇄적으로 로드한다
- 최종 높이가 런타임에 결정된다
- 메인 스레드에서 스크립트가 실행되어 렌더링을 방해한다
해결
- 슬롯 높이를 미리 예약(placeholder)
iframe는 가능한 한 고정 크기- 서드파티 스크립트는 지연 로드, 또는 상호작용 이후 로드
<div class="ad-slot" aria-hidden="true"></div>
<script defer src="/ads-loader.js"></script>
.ad-slot {
min-height: 250px; /* 광고가 오기 전에도 공간 확보 */
}
서드파티 문제는 “원인 찾기”가 중요합니다. 이런 류의 디버깅은 로그를 근거로 빠르게 범위를 좁히는 방식이 유효한데, 접근 방식 자체는 장애 원인 추적과 유사합니다. 트러블슈팅 사고법은 systemd 서비스 재시작 루프, 10분 디버깅 글의 흐름을 참고하면 프론트 성능 분석에도 그대로 적용됩니다.
원인 5) 스켈레톤/로딩 UI가 실제 콘텐츠와 크기가 다름
스켈레톤 UI는 체감 성능을 올리지만, 스켈레톤의 높이와 실제 콘텐츠 높이가 다르면 로딩 완료 순간에 레이아웃이 바뀌어 CLS가 발생합니다.
자주 보는 패턴:
- 카드 리스트 스켈레톤은 2줄 가정인데 실제는 3줄
- 이미지 영역은 회색 박스인데 실제는 더 큰 비율
- 버튼/배지 렌더링으로 줄바꿈 발생
해결
- 스켈레톤을 “최종 레이아웃과 동일한 박스 모델”로 설계
- 텍스트는
line-height와 줄 수를 보수적으로 잡고, 넘치면clamp로 제한
.title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
min-height: calc(1.4em * 2); /* 2줄 높이 예약 */
}
스켈레톤은 “그릴수록 안전”이 아니라 “최종 크기와 일치할수록 안전”입니다.
원인 6) 늦은 CSS 로드와 렌더링 경로 차단: 스타일 적용 시점이 바뀜
CSS가 늦게 로드되면 초기에 unstyled 상태로 그려졌다가(FOUC) 스타일이 적용되며 레이아웃이 크게 바뀔 수 있습니다. 또한 무거운 JS가 메인 스레드를 점유하면, LCP 후보가 준비되어도 페인트가 지연됩니다.
대표적인 경우:
- 핵심 CSS가 번들 뒤쪽에 있고, 초기 렌더에 필요함
@import로 CSS를 연쇄 로딩- 초기 JS가 과도해 스타일 계산과 레이아웃/페인트가 밀림
해결
- Above-the-fold에 필요한 CSS는 우선순위를 높이기
@import지양- 초기 스크립트는 분할 로드, 필요 시
defer/async전략 재검토
<link rel="preload" href="/styles/critical.css" as="style" />
<link rel="stylesheet" href="/styles/critical.css" />
<script defer src="/app.js"></script>
또한 서버 응답이 느리거나 TTFB가 불안정하면 LCP는 구조적으로 불리해집니다. 백엔드나 인프라 레벨의 지연이 의심될 때는 DNS나 네트워크 계층부터 빠르게 배제하는 진단 루틴이 필요합니다. 이런 “원인별 가지치기” 방식은 EKS CoreDNS DNS timeout·SERVFAIL 10분 진단 같은 글의 접근이 그대로 도움이 됩니다.
실전 체크리스트: LCP 느림과 CLS를 함께 잡는 순서
아래 순서로 보면 시행착오가 줄어듭니다.
- LCP 요소가 무엇인지부터 고정
- Performance에서 LCP 이벤트 클릭 후 요소 확인
- LCP 요소가 이미지면
width/height또는aspect-ratio로 공간 예약- 우선순위 올리기:
fetchpriority와 preload 검토
- LCP 요소가 텍스트면
- 폰트 로딩 정책과 preload 확인
- 폴백 폰트 metric 차이로 인한 줄바꿈 점검
- Layout Shift 이벤트 상위 1~3개를 잡기
- 배너/쿠키바/광고 슬롯의 “자리 예약” 여부 확인
- 메인 스레드 블로킹 확인
- Performance에서 Long Task가 LCP 직전 구간에 몰려 있는지
- 마지막으로 Lighthouse로 회귀 테스트
- 개선이 실제로 수치에 반영되는지 확인
간단한 RUM 코드로 LCP와 CLS 수집하기
실험실 데이터만으로는 사용자 환경을 대표하기 어렵습니다. web-vitals로 필드 데이터를 수집하면 “특정 브라우저/디바이스에서만 느린” 케이스를 빨리 찾을 수 있습니다.
import { onLCP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
navigator.sendBeacon(
'/vitals',
JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
rating: metric.rating,
navigationType: metric.navigationType,
})
);
}
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
수집된 데이터로 “LCP 상위 구간에서 CLS도 같이 높은지”를 교차 분석하면, 이번 글의 6가지 원인 중 어디를 먼저 때려야 하는지 우선순위가 선명해집니다.
마무리
Chrome에서 LCP가 느리고 Layout Shift가 보인다면, 대부분은 “공간 예약 실패”와 “늦은 적용(폰트, CSS, 동적 삽입)”의 조합입니다. 이미지와 폰트, 배너/광고 슬롯만 제대로 고정해도 CLS는 눈에 띄게 내려가고, LCP도 함께 당겨지는 경우가 많습니다.
중요한 건 한 번에 다 고치려 하지 말고, Performance 트레이스에서 LCP 요소와 Layout Shift 상위 이벤트를 먼저 특정한 뒤, 위 6가지 원인에 대입해 빠르게 제거하는 것입니다. 이런 방식으로 접근하면 ‘체감으로 느린’ 문제를 ‘증거로 느린’ 문제로 바꿔 재현과 개선을 반복할 수 있습니다.