Published on

CSS contain·content-visibility로 렌더링 최적화

Authors

웹 성능 최적화에서 자주 놓치는 지점이 렌더링 파이프라인입니다. 네트워크와 자바스크립트 최적화가 어느 정도 끝난 뒤에도, 스크롤이 버벅이거나 입력 지연이 생기는 경우가 많습니다. 이때 원인은 대개 스타일 계산, 레이아웃, 페인트가 불필요하게 넓은 영역으로 전파되는 것입니다.

CSS의 containcontent-visibility는 이 전파 범위를 강제로 줄여, 브라우저가 더 적은 영역만 다시 계산하도록 만드는 도구입니다. 특히 긴 리스트, 대시보드 위젯, 카드 그리드, 댓글 영역처럼 반복되는 UI에서 효과가 큽니다.

이 글에서는 두 속성의 역할 차이, 안전한 적용 패턴, 흔한 함정, 그리고 DevTools에서 검증하는 방법까지 실전 관점으로 정리합니다.

관련해서 스크롤/컴포지팅 이슈를 더 깊게 파고들고 싶다면 Safari 스크롤 잔상? Compositor 레이어 디버깅도 함께 보면 좋습니다.

렌더링 비용이 커지는 전형적인 패턴

다음 같은 UI는 렌더링 비용이 쉽게 폭증합니다.

  • 무한 스크롤 리스트에서 항목이 수백, 수천 개 쌓임
  • 카드 내부에 이미지, 섀도우, 블러, 그라데이션 등 페인트 비용이 큰 스타일이 많음
  • 한 카드의 상태 변화가 부모 레이아웃 전체에 영향을 줌
  • position: sticky 헤더, 복잡한 그리드 레이아웃, 중첩된 flex 컨테이너

문제는 “화면에 보이지 않는 영역”까지 브라우저가 레이아웃/페인트 계산을 하거나, 작은 변경이 DOM 상위로 전파되어 큰 범위를 다시 계산하게 되는 것입니다.

이때 목표는 간단합니다.

  • 컴포넌트 단위로 렌더링 영향을 격리한다
  • 화면 밖 콘텐츠는 가능하면 건너뛴다

contain은 전자의 성격이 강하고, content-visibility는 후자를 직접 해결합니다.

contain 한 번에 이해하기

contain은 요소를 “독립된 섬”처럼 취급하도록 브라우저에 힌트를 줍니다. 즉, 해당 요소 내부 변경이 바깥 레이아웃/페인트/스타일 계산으로 번지지 않게 경계를 치는 속성입니다.

contain 값 종류와 의미

  • contain: layout;
    • 내부 레이아웃 변화가 외부 레이아웃에 영향을 주지 않도록 격리
  • contain: paint;
    • 요소의 페인트 영역을 자신의 박스 안으로 제한
    • 오버플로우로 밖에 그려지는 효과를 제한할 수 있음
  • contain: style;
    • 스타일 계산의 영향을 격리
    • 커스텀 프로퍼티 상속 등과의 상호작용을 고려해야 함
  • contain: size;
    • 요소 크기가 내부 콘텐츠에 의해 결정되지 않는다고 가정
    • 잘못 쓰면 레이아웃이 깨지기 쉬워 가장 주의가 필요
  • contain: content;
    • layout + paint + style의 축약
  • contain: strict;
    • size + layout + paint + style의 강한 축약

실무에서 가장 안전하게 시작하기 좋은 조합은 보통 contain: content; 또는 contain: layout paint; 입니다. size가 들어가는 순간 “브라우저가 크기를 추론하지 않는다”는 가정이 생겨, 의도치 않은 높이 0 같은 문제가 생길 수 있습니다.

예제: 카드 컴포넌트 격리로 리플로우 전파 줄이기

아래는 카드 내부 상태 변화가 주변 레이아웃에 영향을 덜 주도록 하는 패턴입니다.

.card {
  contain: layout paint;
  border-radius: 12px;
  background: white;
  box-shadow: 0 6px 20px rgba(0,0,0,0.08);
}

.card__title {
  font-weight: 600;
}

이렇게 하면 카드 내부에서 텍스트가 토글되거나, 내부 요소가 늘었다 줄었다 해도 레이아웃 계산이 페이지 전체로 퍼질 확률이 줄어듭니다.

contain: paint;가 부작용을 만들 수 있는 경우

paint는 “요소 박스 밖으로 그리지 말라”는 의미가 있어, 다음과 같은 UI에서 시각적 잘림이 생길 수 있습니다.

  • 드롭다운 메뉴가 부모 박스 밖으로 열리는 경우
  • 툴팁, 팝오버가 카드 밖으로 떠야 하는 경우
  • box-shadow가 바깥으로 크게 퍼지는 디자인

해결은 보통 둘 중 하나입니다.

  • 팝오버를 포털로 body 하위에 렌더링
  • contain을 팝오버를 감싸지 않는 더 안쪽 래퍼에만 적용

content-visibility로 “보이는 것만” 렌더링하기

content-visibility는 화면 밖 콘텐츠의 렌더링 작업을 과감히 스킵하는 데 초점이 있습니다.

핵심 동작

  • content-visibility: auto;는 요소가 뷰포트 근처에 없으면 내부 렌더링을 생략합니다.
  • 스크롤로 근처에 오면 그때 렌더링합니다.

긴 문서, 아코디언, 무한 리스트에서 특히 강력합니다. 단순히 “lazy load 이미지” 수준이 아니라, 스타일 계산과 레이아웃, 페인트 자체를 늦추는 것이기 때문입니다.

반드시 같이 써야 하는 contain-intrinsic-size

화면 밖일 때 렌더링을 스킵하면, 브라우저는 해당 요소의 실제 높이를 모릅니다. 높이를 모르면 스크롤바 길이가 흔들리거나, 스크롤 점프가 생길 수 있습니다.

그래서 거의 항상 contain-intrinsic-size를 함께 지정해 “대략적인 자리”를 확보합니다.

.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: 320px;
}
  • 320px는 평균적인 아이템 높이로 잡습니다.
  • 실제 높이와 크게 다르면, 근처에 왔을 때 레이아웃 보정이 커져 스크롤 감각이 어색해질 수 있습니다.

예제: 무한 스크롤 리스트 최적화

<ul class="feed">
  <li class="feed-item">...</li>
  <li class="feed-item">...</li>
  <li class="feed-item">...</li>
</ul>
.feed {
  margin: 0;
  padding: 0;
  list-style: none;
}

.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: 280px;

  /* 내부 변화가 외부로 전파되는 것도 함께 줄이고 싶다면 */
  contain: layout paint;

  margin: 12px 0;
  padding: 16px;
  border: 1px solid rgba(0,0,0,0.08);
  border-radius: 12px;
  background: #fff;
}

이 조합의 의도는 명확합니다.

  • content-visibility로 화면 밖 렌더링 자체를 미룸
  • contain으로 화면 안에서의 변경도 외부로 덜 번지게 함

contain vs content-visibility 언제 무엇을 쓰나

contain이 먼저인 경우

  • 화면 안에 항상 보이는 위젯이지만 내부 업데이트가 잦음
  • 컴포넌트 경계를 확실히 해 재계산 범위를 줄이고 싶음

예: 실시간 차트 카드, 상태가 자주 바뀌는 통계 위젯, 토글이 많은 설정 패널

content-visibility가 먼저인 경우

  • 긴 페이지에서 아래쪽 콘텐츠가 많음
  • 탭 패널, 아코디언처럼 “현재는 숨겨진 덩어리”가 큼

예: 댓글 2천 개, 문서형 페이지의 섹션 다수, 상품 상세의 추천 영역

함께 쓰면 좋은 경우

  • 반복되는 아이템 단위로 렌더링 비용이 크고, 리스트가 김

다만 “무조건 적용”은 금물입니다. 특히 인터랙션 요소가 많은 영역에서 content-visibility: auto;는 포커스 이동이나 스크린리더 동작이 기대와 다르게 느껴질 수 있으니, 적용 범위를 명확히 통제하는 편이 안전합니다.

성능 측정과 검증 방법

최적화는 적용보다 검증이 더 중요합니다. 다음 순서로 확인하면 시행착오가 줄어듭니다.

1) Chrome DevTools Performance로 리플로우/페인트 감소 확인

  • Performance 탭에서 스크롤을 녹화
  • Rendering 관련 이벤트에서 Recalculate Style, Layout, Paint의 총량을 비교
  • 최적화 전후로 “긴 작업”이 줄었는지 확인

2) Rendering 패널로 페인트 영역 보기

  • DevTools의 Rendering 패널에서 Paint flashing을 켜기
  • 스크롤 시 화면 전체가 번쩍이면 페인트 범위가 과도한 것
  • contain: paint;나 컴포넌트 구조 개선으로 줄이는 방향을 탐색

3) 실사용 환경에서의 체감

로컬에서만 빠르고 실기기에서 느린 경우가 흔합니다. 특히 저사양 안드로이드, 구형 iOS에서 차이가 크게 납니다. 스크롤 잔상이나 레이어 문제는 브라우저별로 양상이 다를 수 있는데, 이 부분은 Safari 스크롤 잔상? Compositor 레이어 디버깅에서 다룬 방식대로 레이어와 컴포지터 관점으로 확인하면 원인 파악이 빨라집니다.

실전 적용 체크리스트

적용하기 좋은 타겟

  • 피드 아이템, 카드, 행 단위 반복 UI
  • 아래쪽에 길게 이어지는 문서 섹션
  • 접혀 있는 아코디언의 본문 덩어리

피해야 할 타겟

  • 팝오버/드롭다운처럼 부모 밖으로 그려져야 하는 요소를 감싸는 컨테이너에 contain: paint; 적용
  • 높이가 크게 들쑥날쑥한데 contain-intrinsic-size를 대충 잡은 경우
  • 스크롤 앵커링이 중요한 영역에서 과도한 content-visibility 적용

권장 롤아웃 방식

  • 먼저 반복 아이템에 content-visibility: auto; + 적절한 contain-intrinsic-size 적용
  • 다음으로 내부 업데이트가 잦은 컴포넌트에 contain: layout paint;를 제한적으로 추가
  • Performance 프로파일링으로 개선 폭이 작은 곳은 되돌리기

자주 묻는 질문

Q. content-visibility: auto;만 쓰면 되지 않나요

대부분의 긴 리스트에는 효과가 있지만, 자리 확보를 위한 contain-intrinsic-size가 없으면 스크롤 점프나 레이아웃 흔들림이 생길 수 있습니다. 또한 “항상 보이는 영역”의 리플로우 전파를 줄이는 데는 contain이 더 직접적입니다.

Q. 가상 스크롤 라이브러리 대신 content-visibility로 충분한가요

케이스에 따라 다릅니다.

  • DOM 노드 자체를 줄여야 할 정도로 수만 개 단위면 가상 스크롤이 여전히 유리합니다.
  • 수백~수천 개 수준에서 “일단 체감 성능”을 빠르게 끌어올리는 데는 content-visibility가 구현 비용 대비 효율이 좋습니다.

마무리

containcontent-visibility는 “브라우저에게 렌더링 경계를 알려주는” 성능 도구입니다. 자바스크립트를 덜 돌리는 최적화가 아니라, 렌더링 계산 자체를 줄이는 접근이라서 스크롤 성능과 입력 반응성이 눈에 띄게 좋아질 수 있습니다.

실무에서는 다음 한 줄부터 시작하는 것이 가장 안전합니다.

  • 긴 리스트 아이템에 content-visibility: auto;contain-intrinsic-size를 추가

그리고 카드나 위젯처럼 내부 업데이트가 잦은 컴포넌트에만 contain: layout paint;를 점진적으로 붙여가며, DevTools로 실제로 LayoutPaint가 줄었는지 확인해보세요.

프론트 성능 최적화는 “많이 적용”보다 “정확히 적용”이 이깁니다.