- Published on
CSS contain·content-visibility로 LCP 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
웹 성능을 튜닝하다 보면 LCP(Largest Contentful Paint)는 단순히 이미지 최적화만으로는 잘 내려가지 않는 경우가 많습니다. 특히 페이지 상단 히어로가 빠르게 그려져도, 그 아래에 있는 무거운 섹션(피드, 댓글, 추천, 차트, 광고 슬롯 등)이 초기 렌더링 단계에서 레이아웃/페인트/스타일 계산을 크게 잡아먹으면 메인 스레드가 밀리고, 결과적으로 LCP 후보 요소의 페인트 타이밍도 늦어질 수 있습니다.
이 글에서는 CSS의 contain과 content-visibility를 이용해 렌더링 비용을 “격리”하거나 “지연”시켜 LCP를 개선하는 방법을 설명합니다. 핵심은 “보이지 않는 영역의 비용을 초기 렌더링 경로에서 빼고”, “상단 핵심 영역은 예측 가능하게 안정화”하는 것입니다.
관련해서 LCP가 느릴 때 원인을 추적하는 방법은 아래 글도 함께 보면 좋습니다.
LCP와 렌더링 비용의 관계
LCP는 뷰포트 내에서 가장 큰 콘텐츠 요소(대개 히어로 이미지, 큰 헤딩, 카드 등)가 “실제로 그려진 시점”을 측정합니다. 중요한 점은 다음입니다.
- LCP 후보 요소 자체가 가볍더라도, 그 시점까지 브라우저가 처리해야 하는 작업(스타일 계산, 레이아웃, 페인트, 컴포지팅, JS 실행)이 많으면 LCP가 늦어집니다.
- 특히 초기 렌더링에서 “화면 아래” DOM이 크고 복잡하면, 브라우저는 전체 트리에 대해 많은 계산을 수행합니다.
- JS가 긴 작업(롱 태스크)을 만들면 렌더링 파이프라인이 밀리며 LCP가 늦어질 수 있습니다.
따라서 LCP 튜닝은 “LCP 요소 최적화”와 “LCP 시점까지의 전체 렌더링 부담 축소” 두 축으로 봐야 합니다. contain과 content-visibility는 두 번째 축에서 강력합니다.
contain 개념: 렌더링 영향 범위를 잘라내기
contain은 어떤 요소의 레이아웃/페인트/스타일 계산이 외부로 전파되는 범위를 제한해, 브라우저가 최적화하기 쉽게 만드는 힌트입니다. 값은 여러 종류가 있고 조합 가능합니다.
contain: layout;레이아웃 영향 범위를 격리contain: paint;페인트 범위를 격리(클리핑 성격)contain: size;크기 계산을 격리(주의 필요)contain: style;스타일 계산 격리(일부 경우)contain: content;보통layout paint style조합에 가까운 프리셋contain: strict;더 강한 격리(보통size layout paint style에 가까움)
LCP 관점에서 자주 쓰는 건 contain: content; 또는 contain: layout paint; 계열입니다. 목적은 “무거운 섹션이 페이지 전체 레이아웃/페인트에 파급을 주지 않게” 만드는 것입니다.
언제 contain이 효과적인가
- 카드 리스트/피드처럼 반복되는 컴포넌트가 많고, 각 컴포넌트가 독립적으로 렌더링 가능한 경우
- 아래쪽 섹션이 복잡한데, 상단 히어로의 레이아웃 안정성을 해치지 않아야 하는 경우
- 애니메이션/스크롤에 따라 자주 다시 그려지는 영역을 격리해 페인트 범위를 줄이고 싶은 경우
contain 적용 예시
아래는 “아래쪽 추천 섹션”이 초기 렌더링에서 과도한 비용을 만들 때, 해당 섹션의 영향을 격리하는 예시입니다.
/* 화면 아래에 있는 무거운 섹션(추천/댓글/피드 등) */
.below-the-fold {
contain: layout paint;
}
/* 반복 카드가 많을 때 각 카드 단위로 격리 */
.feed-card {
contain: content;
}
contain: layout paint;는 비교적 안전한 편이고, 카드나 섹션이 외부 레이아웃에 강하게 의존하지 않는 구조에서 잘 먹힙니다.
content-visibility 개념: 화면 밖 렌더링을 미루기
content-visibility는 요소가 뷰포트 밖에 있을 때, 그 요소의 렌더링 작업(레이아웃/페인트/스타일 계산)을 “스킵”하거나 “지연”할 수 있게 합니다.
content-visibility: auto;화면 밖이면 렌더링을 건너뛰고, 가까워지면 렌더링
이게 LCP에 강력한 이유는, 초기 로드 시점에 “아래쪽의 큰 DOM”을 사실상 렌더링 경로에서 제외할 수 있기 때문입니다.
필수 짝꿍: contain-intrinsic-size
content-visibility: auto;를 적용하면, 화면 밖 콘텐츠는 렌더 트리에서 스킵되며 높이를 모르게 됩니다. 이때 스크롤바가 갑자기 변하거나, 요소가 뷰포트에 들어오는 순간 레이아웃 점프가 발생할 수 있습니다.
이를 완화하기 위해 contain-intrinsic-size로 “대략적인 자리(placeholder 크기)”를 잡아줍니다.
/* 화면 아래 섹션을 통째로 지연 렌더링 */
.section-heavy {
content-visibility: auto;
contain-intrinsic-size: 800px; /* 대략적 높이. 실제 평균값으로 조정 */
}
높이를 정확히 맞출 필요는 없지만, 너무 작으면 들어오는 순간 큰 레이아웃 변경이 생기고, 너무 크면 스크롤 길이가 과하게 잡힙니다. “평균치” 또는 “최소 기대치”를 기준으로 잡고 관측하면서 조정하는 방식이 현실적입니다.
LCP 튜닝 전략: 어디에 적용할 것인가
무작정 모든 요소에 content-visibility: auto;를 걸면 오히려 역효과가 날 수 있습니다. LCP를 목표로 한다면 “LCP 후보 영역은 건드리지 말고”, “LCP 이후에 등장하는 무거운 영역을 지연/격리”하는 접근이 안전합니다.
1) LCP 후보 요소가 있는 히어로 영역은 안정화
- 히어로 이미지/헤딩이 들어있는 컨테이너에는
content-visibility를 적용하지 않는 것이 일반적입니다. - 대신 히어로 주변의 레이아웃 변동을 줄이는 것이 중요합니다(이미지
width/height지정, 폰트 로딩 전략 등).
2) 아래 폴드의 “큰 덩어리”부터 content-visibility 적용
- 댓글 섹션, 추천 목록, 관련 상품, 무한 스크롤 피드, 대형 테이블/차트
- 광고 슬롯이 DOM을 크게 만들거나, 서드파티 위젯이 무거운 경우
/* 상세 페이지 예시 */
.product-detail__reviews,
.product-detail__recommendations,
.product-detail__qna {
content-visibility: auto;
contain-intrinsic-size: 600px;
}
3) 반복 컴포넌트는 contain으로 격리
content-visibility는 “화면 밖”에 강하고, contain은 “화면 안에서도 반복되는 복잡한 컴포넌트”에 유용합니다.
/* 화면 안에 보이더라도 카드가 많으면 격리로 비용을 줄인다 */
.grid-card {
contain: layout paint;
}
실전 예제: Next.js 페이지에서 적용 패턴
아래는 Next.js 같은 SPA/SSR 환경에서 흔한 “상단 히어로 + 아래 피드/댓글” 구조를 가정한 예시입니다.
<main>
<section class="hero">
<h1>가장 중요한 헤딩</h1>
<img src="/hero.jpg" width="1200" height="630" alt="hero" />
</section>
<section class="feed section-heavy">
<!-- 카드 수십 개 -->
</section>
<section class="comments section-heavy">
<!-- 댓글 위젯 -->
</section>
</main>
.hero {
/* LCP 후보 영역은 지연시키지 않는다 */
}
.section-heavy {
content-visibility: auto;
contain-intrinsic-size: 900px;
}
.feed .card,
.comments .comment {
contain: layout paint;
}
이 조합은 초기 렌더링에서 아래 폴드의 비용을 크게 줄이면서, 스크롤 시점에는 자연스럽게 렌더링되도록 유도합니다.
흔한 부작용과 회피 방법
레이아웃 점프(CLS) 증가
- 원인:
contain-intrinsic-size가 너무 작거나, 섹션별 높이 편차가 큰데 동일 값으로 고정 - 해결: 섹션 타입별로 다른 intrinsic size를 두고, 실제 사용자 데이터(평균 높이)에 맞춰 조정
.section-heavy--feed {
content-visibility: auto;
contain-intrinsic-size: 1200px;
}
.section-heavy--comments {
content-visibility: auto;
contain-intrinsic-size: 700px;
}
스크롤 시점에 “갑자기 로딩되는 느낌”
- 원인: 뷰포트에 들어오는 순간 렌더링이 시작되어, 첫 진입 프레임이 무거울 수 있음
- 해결: 중요한 바로-아래 섹션은 지연하지 말고, 더 아래 섹션부터 적용하거나, 섹션을 더 잘게 쪼개 적용
contain: size;의 과도한 사용
contain: size;는 요소 크기 계산을 격리해 강력하지만, 자식 콘텐츠 크기에 따라 부모가 늘어나야 하는 레이아웃에서는 의도치 않은 결과를 만들 수 있습니다.- LCP 목적이라면 일단
layout paint또는content위주로 시작하고, 문제가 없을 때만 점진적으로 강화하는 편이 안전합니다.
측정과 검증 루틴(DevTools 중심)
튜닝은 “적용”보다 “검증”이 더 중요합니다. 다음 순서가 실무에서 재현성이 좋습니다.
- Chrome DevTools Performance 패널로 기록
- LCP 이벤트 타이밍과 메인 스레드 롱 태스크 확인
- 초기 렌더링 구간에서 아래 폴드 섹션이 Layout/Update Layer Tree/Paint를 얼마나 유발하는지 확인
content-visibility적용 후 동일 조건에서 다시 기록- LCP가 당겨졌는지, 대신 CLS가 늘었는지 함께 확인
렌더-블로킹 리소스나 초기 로딩 병목을 먼저 잡아야 하는 케이스도 많습니다. 추적 방법은 아래 글이 도움됩니다.
추천 적용 체크리스트
- LCP 후보 요소(히어로)는
content-visibility로 지연하지 않는다 - 화면 아래 “큰 덩어리” 섹션부터
content-visibility: auto;적용 - 반드시
contain-intrinsic-size로 자리 확보(섹션별로 값 분리 권장) - 반복 컴포넌트는
contain: layout paint;로 격리해 페인트/레이아웃 파급을 줄인다 - 적용 후 LCP뿐 아니라 CLS, INP(상호작용 지연)도 같이 본다
마무리
contain과 content-visibility는 이미지 압축이나 코드 스플리팅처럼 “리소스를 줄이는 최적화”가 아니라, 브라우저가 렌더링 작업을 더 똑똑하게 건너뛰거나 격리하도록 만드는 “렌더링 파이프라인 최적화”입니다. 특히 DOM이 크고 컴포넌트가 많은 서비스에서 LCP 개선 폭이 크게 나오는 경우가 많습니다.
다만, 성능 지표는 트레이드오프가 생기기 쉽습니다. content-visibility로 LCP를 줄였는데 contain-intrinsic-size 설정이 부정확해 CLS가 늘어나는 식입니다. 따라서 “섹션 단위로 작게 적용하고, 측정으로 확인하며 확대”하는 방식이 가장 안전하고 재현성 있는 LCP 튜닝 루틴입니다.