화면 밖 목록의 렌더를 건너뛴다 — content-visibility 로 레이아웃 속도를 비약적으로 끌어올리기

  • 9th September 2026
  • 7 min read

목록 화면에 항목이 수천 개 있다. 사용자 화면에 실제로 보이는 것은 열 개쯤이다. 그런데 브라우저는 보이지 않는 나머지까지 전부 레이아웃하고 페인트한다. 각 항목의 높이를 계산하고, 문서 전체 높이를 구하고, 스크롤바 길이를 정한다.

이 낭비를 줄이는 방법은 오랫동안 자바스크립트였다. 화면에 보이는 만큼만 DOM에 넣고 스크롤에 따라 갈아 끼우는 가상 스크롤이다. 잘 동작하지만 직접 구현하면 스크롤 위치 복원, 키보드 이동, 검색, 인쇄가 차례로 문제가 된다.

content-visibility는 같은 일을 CSS 한 줄로 시킨다.

.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 132px;
}

2025년 9월 15일 사파리 26이 합류하면서 Baseline에 들어왔다. 크롬은 108(2022년 11월), 파이어폭스는 130(2024년 9월)부터 지원했으니 사파리를 3년 가까이 기다린 기능이다.

무엇이 얼마나 줄어드는가

카드 2만 개짜리 페이지(HTML 2.8MB)를 만들어 크롬 152 헤드리스에서 로드 시간을 쟀다. 프로세스 시작 비용과 HTML 파싱 비용을 분리해야 의미가 있으므로, 기준선을 두 개 두었다 — 빈 페이지, 그리고 같은 HTML을 전부 display:none으로 만든 것(파싱은 하고 레이아웃은 하지 않는 상태)이다.

측정 대상 전체 레이아웃·페인트분
빈 페이지 (프로세스 시작·종료)1,691ms
전부 display:none (파싱만)2,168ms기준선
그냥 렌더2,476ms308ms
content-visibility: auto2,307ms139ms
auto + contain-intrinsic-size2,189ms21ms

레이아웃·페인트만 보면 308ms에서 21ms로 줄었다. 93% 감소다. 그런데 전체 로드 시간은 2,476ms에서 2,189ms로 12%만 줄었다. 이 간격이 중요하다.

content-visibility는 HTML 파싱과 DOM 생성 비용을 줄이지 않는다. 2만 개 요소를 파싱해 DOM 트리를 만드는 477ms는 그대로 남는다. 줄어드는 것은 그 DOM에 대한 레이아웃과 페인트뿐이다. "초기 로드 성능이 크게 개선된다"는 소개를 그대로 받아들이면 안 되는 이유다. 페이지에서 레이아웃 비중이 클 때만 크게 개선된다.

거꾸로 말하면, DOM 자체가 무거워서 느린 페이지에는 이것만으로 부족하다. 요소 수를 줄이는 것(서버 페이징, 가상 스크롤)이 여전히 유효하고, content-visibility는 그 위에 얹는 층이다.

측정 조건을 밝혀 둔다. 맥에서 크롬 152 헤드리스로 --dump-dom까지의 벽시계 시간을 5회 재 중위값을 썼다. 헤드리스에는 실제 화면 합성이 없으므로 사용자가 체감하는 스크롤 부드러움과는 다른 지표다. 절대값보다 세 조건의 차이를 보는 용도다.

contain-intrinsic-size 를 빼면 스크롤바가 거짓말한다

위 표에서 contain-intrinsic-size가 있을 때와 없을 때가 139ms 대 21ms로 갈렸다. 이 속성은 성능만의 문제가 아니다. 빼면 스크롤바가 망가진다.

브라우저가 화면 밖 요소의 레이아웃을 건너뛰면 그 요소의 높이를 모른다. 그래서 추정치를 쓴다. 카드 5,000개로 문서 높이를 재 봤다.

설정 문서 높이 실제 대비
그냥 렌더 (실제 높이)602,969px100%
auto만 (크기 미지정)165,000px27%
auto + 132px 지정825,000px137%

크기를 지정하지 않으면 문서가 실제의 4분의 1로 잡힌다. 스크롤바가 실제보다 훨씬 짧은 문서를 가리키고, 스크롤을 내리는 동안 화면에 들어온 카드가 실제 높이로 펴지면서 문서가 계속 길어진다. 스크롤바 손잡이가 계속 줄어들고 스크롤 위치가 튄다.

값을 지정하면 추정이 그 값으로 고정된다. 다만 실제 높이와 다르면 반대 방향으로 어긋난다 — 위 표에서 132px로 잡았더니 문서가 실제보다 37% 길어졌다. 카드 실제 높이가 121px이었기 때문이다.

그래서 값을 손으로 정확히 맞추려 애쓰는 것보다 auto 키워드를 함께 쓰는 편이 낫다.

contain-intrinsic-size: auto 132px;

MDN의 설명은 이렇다. "섹션이 한 번 렌더된 뒤에는, 뷰포트 밖으로 스크롤되어도 렌더된 내재 크기를 유지한다." 뒤의 132px아직 한 번도 렌더되지 않은 요소에만 쓰이는 초기 추정값이고, 한 번 화면에 들어온 요소는 그때 측정된 실제 크기를 기억한다. 스크롤을 내려갔다 올라오는 동안 높이가 흔들리지 않는다.

초기 추정값은 대표적인 항목 하나를 개발자 도구로 재서 넣으면 충분하다. 정확하지 않아도 되고, 없는 것보다 훨씬 낫다.

함정 1 — 카드 밖으로 나가는 것이 잘린다

실무에서 가장 자주 걸리는 자리다. content-visibility: auto레이아웃·스타일·페인트 컨테인먼트를 함께 켠다. 그중 페인트 컨테인먼트가 자손이 요소 경계 밖으로 그려지는 것을 잘라낸다.

목록 카드에 걸었다면, 카드 안에 있는 드롭다운 메뉴·툴팁·팝오버가 카드 경계에서 잘린다.

일반 카드 카드 드롭다운이 온전히 보인다 content-visibility: auto 카드 드롭다운이 여기부터 안 보인다 카드 경계에서 잘린다 — 점선은 원래 위치 요소 위치는 그대로다. getBoundingClientRect 는 잘리기 전 좌표를 그대로 돌려주므로 스크립트로는 잘림을 감지할 수 없다. 눈으로 봐야 드러난다.
크롬 152에서 실측한 모습 — 같은 마크업, 카드에 content-visibility 만 추가

주의할 점이 하나 더 있다. 요소의 좌표는 바뀌지 않는다. getBoundingClientRect()로 재면 잘리기 전 위치를 그대로 돌려준다. 클리핑은 그리는 단계에서 일어나기 때문이다. 그래서 이 문제는 테스트 코드로 잡히지 않는다. 실제로 열어 봐야 드러난다.

대응은 구조를 바꾸는 것이다. 카드 밖으로 나가야 하는 UI는 카드 안에 두지 않는다. <dialog>나 팝오버로 문서 최상위에 띄우거나, 앵커 포지셔닝으로 카드 밖에 두고 위치만 카드에 맞춘다. 또는 content-visibility를 카드가 아니라 카드 여러 개를 묶은 구획에 걸어 잘림 경계를 밖으로 밀어낸다.

함정 2 — position: fixed 의 기준이 카드로 바뀐다

레이아웃 컨테인먼트는 그 요소를 고정 위치 자손의 컨테이닝 블록으로 만든다. 화면 기준으로 붙어 있어야 하는 요소가 카드 기준으로 붙는다.

같은 마크업에서 카드 안의 position: fixed 요소 위치를 재 봤다.

일반 카드 안의 fixed          top = 0     (화면 위쪽에 붙음)
content-visibility 카드 안    top = 331   (카드 위치에 붙음)

목록 항목마다 화면 우상단에 뜨는 알림 배지나 고정 툴바를 카드 안에 두었다면 이 자리에서 어긋난다. 잘림과 같은 처방이 듣는다 — 문서 최상위로 옮긴다.

함정 3 — 화면 밖 콘텐츠를 스크립트로 읽을 수 없다

렌더를 건너뛴 콘텐츠는 텍스트로도 잡히지 않는다. 카드 5,000개 중 마지막 카드에서 innerText를 읽어 보면 이렇다.

그냥 렌더               innerText.length = 34
content-visibility     innerText.length = 0

innerText는 "화면에 보이는 대로의 텍스트"를 돌려주는 속성이라, 렌더되지 않은 콘텐츠는 빈 문자열이 된다. 화면 밖 항목의 텍스트를 스크립트로 훑어 필터링하거나 길이를 재던 코드가 조용히 빈 값을 받는다.

textContent는 렌더와 무관하게 DOM에서 읽으므로 영향을 받지 않는다. 화면 밖 콘텐츠를 다루는 코드는 innerText 대신 textContent를 쓴다.

높이를 재는 코드도 조심해야 한다. getBoundingClientRect()를 호출하면 브라우저가 그 자리에서 레이아웃을 계산해 값을 준다. 값은 얻지만 건너뛰려던 작업을 강제로 시키는 것이라, 목록 전체를 훑으며 크기를 재는 코드가 있으면 content-visibility의 이득이 사라진다.

auto 와 hidden 은 쓰는 자리가 다르다

content-visibility에는 값이 하나 더 있다. hidden은 화면에 들어와도 렌더하지 않는다. 스크립트로 다시 켤 때까지 감춰 둔다.

auto hidden
화면에 들어오면저절로 렌더된다계속 감춰진다
브라우저 검색(찾기)찾아진다안 찾아진다
탭 이동·포커스된다안 된다
선택(드래그)된다안 된다

이 표가 auto가 가상 스크롤보다 나은 이유를 그대로 보여준다. 자바스크립트 가상 스크롤은 화면 밖 항목을 DOM에서 제거하므로 브라우저 찾기로 검색되지 않고 탭 이동도 닿지 않는다. content-visibility: autoDOM에 그대로 두고 그리기만 건너뛴다. 명세는 건너뛴 콘텐츠가 "찾기 같은 사용자 에이전트 기능에는 평소처럼 이용 가능해야 하고", "평소처럼 포커스와 선택이 가능해야 한다"고 요구한다. 접근성 트리에도 남는다.

hidden은 성능 최적화가 아니라 display: none의 대체로 쓰는 값이다. MDN은 차이를 이렇게 적는다. "display: none 대신 content-visibility: hidden을 쓰면 감춰진 동안 콘텐츠의 렌더링 상태가 보존되고 렌더링이 더 빠르다." 탭이나 아코디언처럼 같은 콘텐츠를 반복해 감추고 보여주는 자리에 맞는다. 다시 보일 때 처음부터 다시 만들지 않는다.

어디에 쓰고 누가 효과를 보는가

효과가 나는 조건이 뚜렷하다.

  • 세로로 긴 목록·표·피드 — 항목이 수백 개 이상이고 대부분 화면 밖에 있는 구조. 게시판 목록, 로그 화면, 긴 문서의 절 단위.
  • 항목 하나하나가 무거울 때 — 안에 표·이미지·중첩 레이아웃이 있으면 건너뛰는 이득이 커진다.
  • 느린 기기 — 레이아웃 계산은 CPU 작업이라, 성능 차이가 그대로 체감 차이가 된다. 데스크톱에서 300ms면 저사양 안드로이드에서는 몇 배가 된다. 효과를 가장 크게 보는 쪽은 오래된 폰으로 접속하는 사용자이고, 개발자 장비에서는 차이가 잘 느껴지지 않는다.

반대로 쓸모가 없거나 손해인 자리도 분명하다.

  • 화면에 다 들어오는 페이지 — 건너뛸 것이 없으니 이득이 0이고, 컨테인먼트 부작용만 남는다.
  • 항목이 몇십 개 수준 — 재 볼 가치가 없다. 측정해서 차이가 없으면 넣지 않는다.
  • 카드 밖으로 열리는 UI가 있는 목록 — 잘림 문제를 먼저 해결해야 한다.
  • DOM 자체가 무거워 느린 페이지 — 파싱 비용은 줄지 않는다. 요소 수를 줄이는 쪽이 먼저다.

도입 순서는 이렇게 하면 안전하다. 목록 항목 하나의 실제 높이를 재서 contain-intrinsic-size: auto <그 값>으로 넣는다. 스크롤을 위아래로 오래 움직여 스크롤바가 튀지 않는지 본다. 카드 안에서 열리는 드롭다운·툴팁을 하나씩 열어 잘리지 않는지 눈으로 확인한다. 브라우저 찾기로 화면 밖 항목의 텍스트가 검색되는지 확인한다. 목록을 훑는 스크립트에 innerText가 있는지 검색한다.

정리

  • content-visibility: auto는 화면 밖 요소의 레이아웃과 페인트를 건너뛴다. 2만 개 카드에서 레이아웃·페인트분이 308ms → 21ms로 줄었다.
  • 파싱·DOM 생성 비용은 줄지 않는다. 전체 로드 시간으로는 12% 개선이었다. 레이아웃 비중이 큰 페이지에서만 크게 효과가 난다.
  • contain-intrinsic-size는 선택이 아니다. 없으면 문서 높이가 실제의 27%로 잡혀 스크롤바가 요동친다. auto <length>로 쓰면 한 번 렌더된 크기를 기억한다.
  • 자손이 카드 경계에서 잘린다. 드롭다운·툴팁은 문서 최상위로 옮긴다. 좌표는 그대로라 스크립트로는 감지되지 않는다.
  • position: fixed 자손의 기준이 그 요소로 바뀐다.
  • 화면 밖 콘텐츠의 innerText는 빈 문자열이다. textContent를 쓴다.
  • auto브라우저 찾기와 탭 이동이 그대로 된다 — 자바스크립트 가상 스크롤이 잃는 것을 잃지 않는다. hidden은 성능용이 아니라 display:none의 대체다.

한 줄로 켜지는 최적화지만 컨테인먼트가 함께 딸려 온다. 목록 화면에 걸 때는 성능을 재기 전에 잘리는 것이 없는지부터 보는 편이 낫다. 성능은 나중에 되돌릴 수 있지만, 잘린 드롭다운은 사용자가 먼저 발견한다.

참고