content-visibility: auto를 적용한 3,000개 카드 테스트 결과
요약
대규모 제품 그리드(3,000개 카드) 환경에서 CSS 속성 `content-visibility: auto`를 적용한 테스트 결과를 분석했습니다. 이 속성은 헤드리스 Chromium의 첫 프레임 시간을 144ms에서 53ms로 크게 단축시키는 효과를 보였습니다. 또한, 카드의 렌더링 방식을 최적화하여 레이아웃 시간과 페이지 높이를 현저히 줄이는 것이 중요함을 보여줍니다.
핵심 포인트
- `content-visibility: auto`는 대규모 카드 그리드에서 첫 프레임 속도를 크게 개선합니다.
- 불필요한 요소의 렌더링을 제한하는 것이 성능 최적화에 매우 효과적입니다.
- 레이아웃 및 페이지 높이 관리가 스크롤 성능과 직결됩니다.
-
헤드리스 Chromium 145에서 첫 프레임 시간이 144ms에서 38ms로 감소했습니다.
-
contain-intrinsic-size가 없을 경우, Chromium은 16개가 아닌 444개의 카드를 렌더링했고, 200개 카드 페이지의 속도가 느려졌습니다.
-
360px 높이 추정치는 페이지를 36% 짧게 만들었고 스크롤 복원(scroll restore)을 깨뜨렸습니다.
-
레이아웃 작업은 스크롤 시간으로 이동했습니다: 약 3ms씩 560단계에 걸쳐 1.6초 동안 분산되었습니다.
3,000개의 제품 카드가 있는 페이지에서 하나의 CSS 속성이 헤드리스 Chromium의 첫 프레임 시간을 144ms에서 38ms로 줄였습니다. 대부분의 스니펫이 보여주는 방식으로 작성된 이 속성은 화면에 16개가 표시될 때 444개의 카드를 렌더링했고, 200개 카드 페이지를 아무것도 하지 않은 것보다 느리게 만들었습니다.
저는 Playwright를 사용하여 네 가지 변형을 실행하고 브라우저에서 수치를 읽었습니다. 나쁜 경우에 대한 수정은 두 번째 줄의 CSS이며, 여기에 넣는 높이가 예상했던 것보다 더 중요합니다.
테스트: 헤드리스 Chromium에서의 3,000개 제품 카드
이 페이지는 단순한 제품 그리드입니다. 각 카드는 가로세로 비율(aspect ratio)이 4:5인 이미지 박스, 배지, 제목, 18~34단어의 설명, 작은 태그 행, 버튼이 있는 가격 행으로 구성되어 있습니다. 카드당 총 14개의 요소가 있으며, grid-template-columns: repeat(auto-fill, minmax(240px, 1fr))로 배치되어 가로 1280px 너비에서 네 개의 열을 형성합니다.
Node 스크립트는 실행할 때마다 새 페이지를 열고, innerHTML로 카드를 삽입했으며 두 가지를 측정했습니다. 첫째, 브라우저가 프레임을 생성하는 시간(삽입 후 두 번의 requestAnimationFrame 콜백). 둘째, 레이아웃 및 스타일 시간 (Chromium이 DevTools 프로토콜의 Performance.getMetrics를 통해 보고하는 값). 각 변형당 15회 실행, 중앙값 보고, M3 Pro 노트북에서 Chromium 145 사용, 뷰포트 1280x800입니다.
변형은 .card에 대한 하나의 규칙으로 다릅니다:
/* A: 아무것도 없음 */
...
3,000개 카드에서의 결과:
| Variant | 첫 프레임 (First frame) | 레이아웃 (Layout) | 렌더링된 카드 수 (Cards rendered) | 페이지 높이 (Page height) |
|---|---|---|---|---|
| A, nothing | 144.2 ms | 81.3 ms | 3,000 | 448,033 px |
| ... | ||||
| Variant D는 로드 시 16개의 카드를 렌더링합니다: 화면에 보이는 행과 그 바로 옆의 행들입니다. 나머지 2,984개는 건너뜁니다. 이들은 레이아웃이나 페인트가 적용되지 않지만 DOM에는 남아 있습니다. 레이아웃 시간은 81ms에서 9ms로 떨어지고, 페이지 높이는 실제 값의 0.1% 범위 내에 있게 됩니다. |
또한 동일한 프로토콜을 통해 CPU를 4배 제한(throttled)하여 테스트했습니다. 이는 중급형 휴대폰의 대략적인 대체재일 뿐입니다 (장치가 아닙니다). 격차가 벌어졌습니다: Variant A는 첫 프레임이 640ms였고, Variant D는 161ms였습니다.
C와 D는 속도 면에서 서로 노이즈 범위 내에 있습니다. 차이는 나중에 누군가 스크롤할 때 나타납니다.
모두가 복사하는 스니펫 (The Snippet Everyone Copies)은 444개의 카드를 렌더링했습니다
Variant B는 자체적으로 content-visibility: auto를 적용합니다. 3,000개 카드에서도 여전히 아무것도 하지 않는 것보다 나았으며, 144ms에서 53ms로 떨어졌습니다. 하지만 렌더링된 카드를 보세요: 화면에 8개가 표시되는 데 444개의 카드입니다.
A가 건너뛴 요소는 크기 포함(size containment)을 얻습니다. 크기 힌트가 없으면, 이는 높이가 0픽셀인 것으로 처리되고 테두리가 추가됩니다. 첫 레이아웃 시 전체 3,000개 카드 그리드는 13,516px를 측정했으며, 거의 모두 간격으로 채워져 있었습니다. 수백 개의 평평한 카드가 뷰포트와 브라우저가 주변에 유지하는 마진 안에 들어가서 Chromium은 그중 444개를 렌더링하고, 실제로 얼마나 높은지 파악하여 모든 것을 두 번째로 레이아웃했습니다. 첫 프레임 이후 페이지는 77,886px 높이였고, 제가 스크롤하는 동안 계속 커졌습니다. 열 단계가 지나자, 실제 위치가 1.8이었을 때 스크롤바 손잡이는 트랙의 10.4%에 놓였습니다.
작은 페이지에서 이 두 번째 패스는 건너뛰기가 절약하는 것보다 더 많은 비용이 들었습니다. 전체 CPU 속도로 페이지 크기별 첫 프레임 시간:
| Cards | A, nothing | B, auto only | D, auto 580px |
|---|---|---|---|
| 48 | 11.0 ms | 13.0 ms | 11.2 ms |
| ... | |||
| 200개 카드에서는 복사된 스니펫이 페이지를 더 느리게 만들었습니다. |
4배 스로틀링(throttle) 환경에서도 같은 결과였습니다. 속성(property)이 없을 때는 59.3ms였고, 있을 때는 64.1ms였습니다. Variant D는 동일한 200개 카드 페이지에서 31.6ms를 기록했습니다.
48개의 카드는 Shopify 스토어의 제품 컬렉션 한 페이지 분량으로, 최대 속도에서는 측정할 만한 변화가 없었습니다. 하지만 스로틀링 환경에서 D는 약 6ms를 절약했습니다(30.8ms에서 25.1ms로).
따라서 항목이 몇 백 개 미만일 때는 이 기능을 추가하지 않습니다. 48개 제품 그리드는 보통 이미지나 스크립트 같은 더 큰 문제가 먼저 해결되어야 하며, 실제로 테마의 성능을 개선한 변경 사항은 Shopify Theme Performance: From 62 to 98 Lighthouse in One Weekend에서 확인할 수 있습니다.
높이를 추측하고 브라우저가 기억하게 하라
contain-intrinsic-size는 건너뛰어진 요소에 임시 크기(placeholder size)를 제공합니다. Variant C는 360px로 크기를 예측했습니다. 실제 카드의 평균 크기는 가로 1280px일 때 583px였으므로, 페이지 높이는 448,033px가 아닌 284,836px로 계산되었습니다. 이는 36% 부족한 수치입니다.
스크롤바에서 이를 확인할 수 있습니다. 뷰포트 크기 스크롤 단계를 열 번 진행했을 때, 엄지손가락은 실제 값인 1.8% 대신 트랙의 2.8%에 위치하여 약 56% 과도하게 빠르게 움직였습니다. 실제 크기가 예측값을 대체하면서 페이지 높이는 560단계 중 532단계에서 변경되었습니다.
auto 360px의 auto는 브라우저에게 요소가 다시 건너뛰어질 때 임시값 대신 마지막으로 렌더링된 크기를 기억하고 사용하도록 지시합니다. web.dev에서는 그렇게 문서화되어 있으며, 한 번의 전체 스크롤 후 Variant C의 페이지는 올바른 높이를 갖게 되었습니다. 하나의 특이점은 Chromium 145 버전에서도 auto가 없는 경우에도 기억했다는 것입니다. 다른 엔진들도 같은지 모르기 때문에 저는 계속해서 auto를 작성하고 있습니다.
가장 큰 함정은 스크롤 복원(scroll restoration)이었습니다. 평소대로 카드 2,400까지 아래로 스크롤하여 window.scrollY(359,200) 값을 저장한 후, 새 페이지를 로드하고 그 값으로 scrollTo 함수를 호출했습니다.
-
Variant A는 정확한 카드 위치에 나타났습니다.
-
Variant D는 한 줄 아래, 2,404번이 아닌 2,400번 카드를 보여주었습니다.
-
Variant C의 새 페이지는 너무 짧았습니다. 브라우저가 스크롤을 283,994 px에서 고정했고, 맨 끝 근처인 2,992번 카드가 표시되었습니다.
-
상단 카드의 ID를 저장하고
scrollIntoView를 호출하는 방식은 세 가지 모두 오른쪽 카드에 나타나게 했습니다.
// 페이지를 떠나기 전에 저장하기
const top = [...document.querySelectorAll(".card")]
...
좋은 추측을 하려면, 브레이크포인트에서 몇 개의 렌더링된 카드를 getBoundingClientRect().height로 측정해 보세요. 제 경우 1280px 너비에서는 583px이었고, 390px 너비에서는 631px이었습니다. 따라서 이 값은 미디어 쿼리에 포함되어야 합니다:
.card { content-visibility: auto; contain-intrinsic-size: auto 630px; }
@media (min-width: 1024px) {
...
스크롤할 때 작업이 발생하는 위치
건너뛴(Skipped) 작업은 카드가 화면에 나타나는 순간 돌아옵니다. 얼마나 많은지 확인하기 위해, 저는 각 Variant를 상단부터 하단까지 800px 간격으로 총 560번 스크롤했고, 레이아웃 카운터를 다시 읽었습니다.
Variant A는 사전에 81ms를 사용했기 때문에 스크롤 중 레이아웃에 0ms를 사용했습니다. Variant D는 560단계에 걸쳐 분산된 1,646ms를 사용하여 단계당 약 3ms였습니다. 가장 느린 프레임은 Variant A의 18.4ms 대비 24.6ms였지만, 그 과정 중 95%의 단계는 17.6ms 미만을 유지했습니다. 대부분의 지연된 작업(deferred work)은 일반적인 60 Hz 프레임 안에 들어왔습니다.
3,000개의 카드를 모두 스크롤하는 사용자는 페이지에 총 약 20배 더 많은 레이아웃 작업을 수행하게 합니다. 아무도 3,000개 카드를 모두 스크롤하지 않으며, 이것이 당신이 베팅하는 부분입니다.
또한 이는 INP(Interaction to Next Paint) 개선책도 아닙니다. Lab에서는 이미 현장에서 시도해 보았습니다: 실제 매장의 폴드 아래 섹션에서 INP는 움직이지 않았는데, 느린 부분이 상호작용 핸들러의 JavaScript 때문이었습니다. 해당 글은 Six Months of Shopify Web Vitals Data What Actually Moved INP입니다. 이것을 로드 시간 도구로 간주하세요.
Skipped content는 DOM과 접근성 트리(accessibility tree)에 남아 있으며, MDN에 따르면 페이지 내 검색(find-in-page) 및 탭 순서(tab order)에서 여전히 사용 가능합니다. 할 수 없는 것은 스킵된 요소(skipped element)에게 실제 크기를 요청하는 것입니다. 떨어진 위치의 카드가 360px로 추정했을 때, 스킵 상태에서는 (추정치 + 상하단 1px 테두리)를 더해 362px을 보고했습니다. 마스너리 레이아웃(masonry layout)이나 고정 오프셋(sticky offset)처럼 화면 밖에 있는 요소를 측정하는 모든 스크립트는 이 자리 표시자 값(placeholder)을 받게 됩니다. 먼저 확인하세요:
const skipped = !card.firstElementChild.checkVisibility({ contentVisibilityAuto: true });
이 검사가 제가 처음에 렌더링된 16개 카드를 센 방법입니다. 폴링(polling) 대신 반응형으로 처리하고 싶다면, contentvisibilityautostatechange가 요소의 렌더링 시작 또는 스킵 상태 중지 시마다 발생합니다.
지원 범위는 이제 넓습니다: web.dev의 표에 따르면 Chrome과 Edge는 85 버전부터, Firefox는 125 버전부터, Safari는 18 버전부터 지원됩니다.
이전 브라우저는 이 속성을 무시하고 모든 것을 렌더링하며, 이는 변형 A(variant A)입니다. 아무것도 깨지지 않습니다.
핵심 요약 (Bottom Line)
content-visibility: auto를 긴 목록에 적용하고 절대 단독으로 사용하지 마세요. 두 번째 줄인 contain-intrinsic-size: auto와 측정된 높이를 함께 사용하는 것이 444개 카드 렌더링을 16개 카드로 바꾸고 페이지 높이를 실제 값의 0.1% 이내로 유지하게 만든 핵심입니다.
제가 사용할 만한 곳은 긴 목록들입니다: 블로그 아카이브, 변경 로그(changelog), 댓글 스레드,
본 기사에는 제휴 링크가 포함되어 있습니다. 이 링크를 통해 가입하시면 추가 비용 없이 제가 소액의 수수료를 받을 수 있습니다. (광고)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기