- Published on
Chrome 렌더링 스로틀링 - contain·will-change 오용
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론부터 결론까지 한 줄로 요약하면, contain과 will-change는 “성능 최적화 스위치”가 아니라 “격리와 레이어 승격을 강제하는 힌트”이며, 과용하면 Chrome이 합성 비용과 메모리 압박을 견디지 못해 오히려 렌더링을 스로틀링(throttling)한다는 점입니다.
특히 디자인 시스템에서 카드, 리스트 아이템, 모달, 토스트 등 재사용 컴포넌트에 습관적으로 will-change: transform 또는 contain: layout paint를 박아두면, 페이지가 커질수록 프레임 드랍이 누적되고 특정 구간에서 갑자기 “느려지는 임계점”을 만나기 쉽습니다.
Chrome 렌더링 스로틀링이란 무엇인가
Chrome의 렌더링 파이프라인은 대략 다음 단계를 반복합니다.
- 스타일 계산
- 레이아웃
- 페인트
- 합성(compositing)
여기서 will-change나 특정 contain 값은 요소를 별도 레이어로 올리거나(합성 레이어), 레이아웃·페인트의 영향 범위를 격리하도록 유도합니다. 의도 자체는 “변경이 잦은 요소를 독립적으로 처리해서 전체 비용을 줄이자”입니다.
문제는, 이 힌트가 지나치게 많이 적용되면 다음 부작용이 커집니다.
- 합성 레이어 수 증가로 인한 합성 비용 증가
- 레이어별 텍스처 메모리(GPU 메모리) 증가
- 레이어 관리 오버헤드 증가(업데이트, 타일링, 업로드)
- 메모리 압박 시 타일 폐기와 재업로드 반복(버벅임)
이 상태에서 Chrome은 일정 조건에서 프레임을 유지하기 위해 작업량을 줄이거나(우선순위 조정), 특정 렌더링 작업을 미루는 형태로 “스로틀링처럼 보이는 현상”이 발생합니다. 개발자가 체감하는 증상은 보통 아래와 같습니다.
- 스크롤이 특정 구간에서 갑자기 무거워짐
transform애니메이션인데도 끊김- hover 효과가 많은 화면에서 마우스 이동만으로 CPU가 치솟음
- 탭 전환 후 다시 돌아오면 첫 스크롤이 유난히 끊김
contain의 의도와 오용 패턴
contain은 “이 요소의 내부는 외부에 영향을 주지 않는다”라는 가정으로 브라우저 최적화를 돕습니다.
contain: layout레이아웃 영향 격리contain: paint페인트 영향 격리(클리핑 포함)contain: size크기 고정 가정contain: content는 보통layoutpaint일부를 포함contain: strict는 더 강한 격리 조합
오용 1: 리스트 아이템 전체에 contain: layout paint를 기본값처럼 적용
긴 피드/상품 리스트에서 모든 아이템에 contain을 주면, 아이템이 많아질수록 격리 단위가 과도하게 쪼개집니다. 격리 자체는 비용을 줄일 수도 있지만, 실제 UI는 다음 요소가 함께 존재합니다.
- 각 아이템 내부에 그림자, 이미지, 그라데이션, 블러
- hover/pressed 상태 전환
- lazy image decode
이때 contain: paint는 클리핑과 별도 페인트 영역 관리가 늘어날 수 있고, 아이템이 계속 생성·삭제되는 가상 스크롤과 결합하면 오히려 페인트/합성의 churn이 커질 수 있습니다.
오용 2: contain: size를 무심코 사용해 레이아웃 점프를 유발
contain: size는 “내부 콘텐츠가 바뀌어도 외부 레이아웃에 영향을 주지 않는다”는 강한 가정입니다. 높이가 변할 수 있는 컴포넌트에 적용하면, 레이아웃이 기대와 다르게 고정되어 오버플로우나 잘림, 또는 스크롤 점프를 만들 수 있습니다.
안전한 가이드
contain은 “정말 격리해도 되는 컴포넌트 경계”에만 적용합니다.- 무한 리스트/피드에서 모든 아이템에 일괄 적용하지 말고, 프로파일링 후 일부에만 적용합니다.
- 대체로
contain: content부터 검토하고, 필요할 때만layoutpaint를 명시합니다.
will-change의 의도와 오용 패턴
will-change는 “곧 바뀔 것”을 브라우저에 알려 선제 최적화를 유도합니다. 대표적으로 transform opacity를 지정하면 합성 레이어 승격이 발생할 수 있습니다.
오용 1: 상시 will-change: transform을 박아두기
많은 팀에서 “애니메이션 부드럽게 하려면 will-change: transform”을 템플릿처럼 사용합니다. 하지만 상시 적용은 다음을 의미합니다.
- 바뀌지도 않는 요소까지 레이어 승격 후보가 됨
- GPU 텍스처 메모리 상주
- 레이어 수 증가로 합성 단계가 무거워짐
특히 카드 200개, 버튼 500개 같은 화면에서 이런 패턴이 누적되면 임계점에서 급격히 느려집니다.
오용 2: will-change: all
all은 거의 항상 과합니다. 실제로 바뀌는 속성이 제한적이라면, 그 속성만 지정해야 합니다.
오용 3: 애니메이션이 끝나도 will-change를 제거하지 않기
will-change는 “잠깐” 쓰고 치우는 것이 기본 전략입니다. 애니메이션이 끝났는데도 레이어를 유지하면, 다음 화면에서도 계속 비용을 냅니다.
증상으로 빠르게 의심하는 체크리스트
다음 중 2개 이상이면 contain 또는 will-change 오용을 강하게 의심할 만합니다.
- 컴포넌트 라이브러리 기본 스타일에
will-change가 들어 있다 - 리스트 아이템, 카드, 셀에
contain이 기본으로 들어 있다 - DevTools Layers 패널에서 레이어가 과도하게 많다
- Performance 프로파일에서 Compositor 관련 작업이 비정상적으로 크다
- 특정 화면에서만 GPU 메모리 사용이 튀고, 스크롤이 끊긴다
DevTools로 레이어 폭증 진단하기
1) Layers 패널로 “승격된 레이어 수” 확인
- DevTools
More tools에서Layers를 열고 - 문제 화면에서 스크롤/호버/애니메이션을 재현합니다.
레이어가 수십, 수백 단위로 생기고 각 레이어가 큰 텍스처를 잡고 있다면 위험 신호입니다.
2) Rendering 탭에서 Paint Flashing과 Layer Borders
- DevTools
Rendering에서Paint flashing을 켜면 - 불필요한 페인트가 어디서 발생하는지 시각화됩니다.
또한 Layer borders를 켜면 어떤 요소가 레이어로 분리되는지 확인할 수 있습니다.
3) Performance에서 합성 비용 확인
Performance녹화 후- Main thread뿐 아니라 Compositor 관련 이벤트, Raster 작업이 과한지 봅니다.
잘못된 예시와 개선 예시
아래는 흔한 “성능 최적화처럼 보이지만 실제로는 독”이 되는 패턴입니다.
잘못된 예시: 모든 카드에 상시 will-change
.card {
will-change: transform;
contain: layout paint;
}
이 코드는 카드가 100개만 넘어도 합성 레이어 후보가 폭증할 수 있습니다.
개선 1: 상시 적용 대신 상호작용 시점에만 부여
.card {
transition: transform 200ms ease, opacity 200ms ease;
}
.card.is-animating {
will-change: transform, opacity;
}
function withWillChange(el, ms = 300) {
el.classList.add('is-animating');
window.setTimeout(() => el.classList.remove('is-animating'), ms);
}
document.querySelectorAll('.card').forEach((card) => {
card.addEventListener('mouseenter', () => {
withWillChange(card, 250);
card.style.transform = 'translateY(-2px)';
});
card.addEventListener('mouseleave', () => {
withWillChange(card, 250);
card.style.transform = 'translateY(0)';
});
});
핵심은 “바뀌는 짧은 구간에만” 힌트를 주는 것입니다.
개선 2: contain은 격리 이득이 명확한 경계에만
예를 들어, 내부에서 레이아웃 변화가 잦지만 외부 레이아웃에는 영향을 주지 않도록 설계된 위젯(고정 크기, 오버플로우 처리)이라면 제한적으로 사용합니다.
.widget {
width: 320px;
height: 180px;
overflow: hidden;
contain: content;
}
여기서도 contain: strict 같은 강한 옵션은 충분히 검증 후 적용합니다.
content-visibility와의 혼동 주의
대규모 리스트 성능 개선을 위해 content-visibility: auto를 고려하는 경우가 많습니다. 이는 오프스크린 렌더링을 건너뛰는 강력한 기능이지만, contain과 함께 쓰일 때 레이아웃 추정과 실제 렌더링 타이밍이 어긋나 “첫 노출 순간 버벅임”이 나타날 수 있습니다.
이 글의 주제는 contain과 will-change지만, 결론적으로 “힌트 계열 속성은 상시 적용이 아니라 조건부 적용 + 측정”이 안전합니다.
팀에서 재발을 막는 실전 규칙
1) 린트 규칙 또는 스타일 가이드로 금지/제한
will-change: all금지- 공용 컴포넌트 기본 스타일에
will-change금지 contain은 사용 사유를 주석으로 남기기
/* OK: hover 시 transform 애니메이션이 있으며 레이어 승격이 실측으로 유효했음 */
.button.is-animating {
will-change: transform;
}
2) 성능 회귀 테스트를 “임계점 데이터”로 관리
이 문제는 보통 데이터가 적을 때는 티가 안 나고, 아이템 수가 늘면 갑자기 터집니다. QA나 자동화에서 “카드 300개, 이미지 300개” 같은 임계점 시나리오를 만들어두는 편이 좋습니다.
대규모 트래픽이나 부하 상황에서의 스로틀링을 다루는 관점은 인프라의 오토스케일링 폭주를 안정화하는 접근과도 닮아 있습니다. 필요하면 EKS HPA 폭주를 KEDA 큐기반 오토스케일링으로 안정화 글처럼 “과잉 반응을 줄이는 설계”를 참고해도 좋습니다.
3) DevTools 스냅샷을 PR 근거로 남기기
- 레이어 수
- GPU 메모리 추정
- Performance trace
이 3가지를 캡처해두면 “최적화처럼 보이는 변경”이 실제로는 퇴행인지 빠르게 판별할 수 있습니다.
결론: contain과 will-change는 ‘상시 최적화’가 아니다
will-change는 짧게, 필요한 요소에만, 애니메이션 구간에만 적용하고 제거합니다.contain은 컴포넌트 경계가 명확하고 격리해도 UX가 깨지지 않는 곳에만 제한적으로 씁니다.- 레이어가 많아지는 최적화는 대개 임계점에서 Chrome 렌더링 스로틀링을 유발합니다.
프론트엔드 성능 문제는 종종 “브라우저 내부 자원 경합”으로 나타나며, 이는 서버에서 락 경합이나 스로틀링이 발생하는 양상과도 비슷합니다. 백엔드에서 병목을 추적하는 방식이 익숙하다면, 프론트에서도 동일하게 “측정하고, 원인을 좁히고, 과잉 힌트를 제거하는” 접근이 가장 빠릅니다. 관련한 병목 추적 관점은 MySQL InnoDB 1205·1213 잠금대기 원인별 해결도 참고할 만합니다.