브라우저는 CSS 변경을 파이프라인을 통해 픽셀로 바꾸는데, 서로 다른 변경은 서로 다른(더 비싸거나 덜 비싼) 단계를 유발합니다.
text
Style → Layout (reflow) → Paint → Composite
- Layout / reflow — 기하 정보(위치/크기) 재계산. 가장 비쌈 — 한 요소를 바꾸면 많은 다른 요소의 재계산을 강제할 수 있음.
브라우저는 CSS 변경을 파이프라인을 통해 픽셀로 바꾸는데, 서로 다른 변경은 서로 다른(더 비싸거나 덜 비싼) 단계를 유발합니다.
Style → Layout (reflow) → Paint → Composite
/* ❌ LAYOUT(reflow) 유발 — 비쌈, 매 프레임 기하 정보 재계산 */
width, height, top, left, margin, padding, font-size
/* ⚠️ PAINT 유발 — 픽셀 다시 그리기 */
color, background, box-shadow, border-radius
/* ✅ COMPOSITE만 유발 — GPU, layout/paint 없음 */
transform, opacity
이것이 가장 중요한 단 하나의 성능 규칙입니다: width/top/margin이 아니라 transform과 opacity를 애니메이션하세요. 레이아웃 속성 애니메이션은 매 프레임 layout을 다시 실행하지만(끊김), transform은 GPU가 합성합니다(부드러운 60fps).
/* ❌ 끊김 */ @keyframes a { to { left: 300px; width: 200px; } }
/* ✅ 부드러움 */ @keyframes b { to { transform: translateX(300px) scaleX(2); } }
.animated { will-change: transform; } /* 미리 자체 GPU 레이어로 승격 */
will-change는 곧 애니메이션할 요소에만 사용하세요 — 남용하면 너무 많은 레이어를 만들어 메모리를 낭비합니다.
// ❌ 읽기-쓰기-읽기-쓰기는 여러 번의 동기 reflow를 강제 */
for (const el of els) { el.style.width = el.offsetWidth + 10 + "px"; }
// ✅ 읽기를 모은 뒤, 쓰기
const widths = els.map(el => el.offsetWidth);
els.forEach((el, i) => el.style.width = widths[i] + 10 + "px");
- contain: layout/paint — 하위 트리를 격리해 변경이 전체 페이지를 reflow하지 않게 함
- content-visibility: auto — 화면 밖 콘텐츠의 렌더링을 건너뜀
- 깊은 selector와 거대한 미사용 스타일시트 최소화
렌더링 성능은 대부분 layout을 반복적으로 유발하지 않는 것에 관한 것입니다.
transform/opacity가 composite 전용(저렴)인 반면 width/top/margin이 reflow를 강제(비쌈)한다는 것을 아는 것 — 더해 layout thrashing을 피하고 contain/content-visibility를 사용하는 것 — 이 애니메이션을 60fps로, 페이지를 빠르게 느끼게 유지하는 비결입니다.