리플로우·리페인트와 transform/GPU 레이어 정리
transform이 빠르다는 말은 자주 들었는데, 정확히 어디서 갈리는지가 늘 모호했다. width를 바꾸면 느리고 translateX는 빠르다는 정도만 외운 채로 코드를 쓰니까, 정말로 어떤 속성이 어느 단계까지 일을 시키는지 설명할 자신이 없었다. 한 번 짚고 가려고 렌더링 파이프라인을 따라가며 정리했다.
리플로우와 리페인트, 한 프레임 안의 다른 단계
브라우저가 한 프레임을 그리는 과정은 대략 Style → Layout → Paint → Composite 순서로 흐른다. 이 중 리플로우와 리페인트는 각각 Layout과 Paint 단계의 비용이다.
리플로우(Reflow)는 DOM 요소의 크기나 위치가 바뀌었을 때 레이아웃을 다시 계산하는 일이다. 한 요소의 width가 바뀌면 옆 요소·자식 요소·부모 요소의 위치까지 줄줄이 다시 계산해야 하니까, 렌더링 과정 중 비용이 가장 크다.
리페인트(Repaint)는 외관은 바뀌었지만 레이아웃에는 영향이 없는 변경, 그러니까 색상·배경·테두리 스타일이 바뀌었을 때 픽셀을 다시 그리는 일이다. 위치 계산은 건너뛰지만 그래도 해당 영역을 다시 칠해야 한다.
위 그림에서 점선이 transform·opacity가 빠지는 지름길이다. 별도 레이어로 분리된 요소라면 Style 직후 바로 Composite으로 넘어가서 Layout·Paint를 건너뛴다.
transform이 리플로우·리페인트를 건너뛰는 이유
transform과 opacity는 별도의 합성 레이어(composite layer)에서 처리된다. 이 레이어는 GPU가 합성하기 때문에, 위치/크기를 transform으로 바꾸는 동작은 기존 픽셀을 그대로 둔 채 합성 단계에서 이동·확대만 다시 한다. Layout과 Paint가 통째로 빠지는 셈이다.
같은 "오른쪽으로 100px 이동"이라도 어떤 속성으로 표현했느냐에 따라 비용이 갈린다.
// 리플로우 발생 - 레이아웃 재계산 필요
element.style.width = '200px';
element.style.left = '100px';
// 리플로우 없음 - GPU 레이어에서 처리
element.style.willChange = 'transform';
element.style.transform = 'scaleX(2) translateX(100px)';위쪽은 width와 left를 직접 바꾸니까 Layout부터 다시 돌아간다. 아래쪽은 width 대신 scaleX(2)로, left 대신 translateX(100px)로 같은 시각 효과를 얻으면서 합성 레이어에서 처리한다. 둘이 만드는 화면은 비슷하지만 한쪽은 한 프레임 안에서 전 파이프라인을 다 돌리고, 다른 쪽은 합성만 다시 한다.
2D transform은 will-change 없이 사전 분리되지 않는다 — 단, JS로 바꿀 때 얘기
여기서 자주 헷갈리는 지점이 있다. transform을 썼다고 항상 합성 레이어가 만들어지는 건 아니다. 3D transform(translate3d, scale3d, rotate3d)이나 perspective가 걸린 변환은 별도 css 지정 없이도 합성 레이어로 승급된다. 그런데 2D transform(translate, scale, rotate)은 조건이 갈린다.
2D transform을 JS로 직접 바꾸는 경우에는 will-change 없이 합성 레이어가 만들어지지 않아 repaint가 일어날 수 있다. 처음 transform이 적용되는 순간에 레이어가 새로 생기면서 다시 칠해야 하기 때문이다. 그래서 자바스크립트로 transform을 조작하는 애니메이션이라면 will-change: transform을 미리 선언해서 레이어 분리를 사전에 시켜두는 편이 안전하다.
다만 CSS 애니메이션/트랜지션으로 구동되는 2D transform은 will-change 없이도 합성 레이어로 승급된다. Chromium의 합성 동작 문서에 따르면, 브라우저가 애니메이션의 시작·끝 상태를 알고 있기 때문에 미리 레이어를 띄울 수 있다. 그러니까 CSS @keyframes나 transition으로 transform을 다루는 경우에는 will-change를 따로 붙이지 않아도 같은 효과를 본다.
요약하면 "2D transform = will-change 필수"가 아니라 "JS로 직접 바꾸는 2D transform = will-change 필요"가 정확하다.
브라우저가 말하는 "레이어"와 GPU의 역할
레이어는 브라우저가 합성(Composite) 단계에서 화면을 그리기 위해 관리하는 단위다. 기본적으로 모든 요소는 하나의 레이어에 속하지만, transform, opacity, will-change, position: fixed 같은 특정 속성이 적용되면 별도의 합성 레이어로 분리된다. 분리된 레이어는 자기 영역의 픽셀을 따로 들고 있다가, 합성 단계에서 GPU가 이걸 하나의 화면으로 겹쳐 그린다.
GPU(Graphic Processing Unit)는 원래 2D/3D 그래픽 렌더링을 위해 만들어진 병렬 연산 프로세서다. CSS의 transform, opacity, 비디오 디코딩 같은 작업은 GPU 쪽으로 위임해서 CPU 부하 없이 빠르게 합성한다. transform이 빠른 이유의 절반은 "Layout·Paint를 건너뛴다"이고, 나머지 절반은 "그 합성 자체를 GPU가 한다"다.
어느 속성이 어디까지 건드리는가
| 단계 | 대표 속성 |
|---|---|
| 리플로우(Layout)를 일으키는 속성 | position, display, width, height, top, left, float, font-family, font-size, font-weight, line-height, min-height, margin, padding, border 등 |
| 리페인트(Paint)만 일으키는 속성 | background, background-image, background-position, border-radius, border-style, color, outline 등 |
box-shadow는 색·테두리 그림자라서 paint-only로 보이기 쉽지만, 그림자가 요소 바깥 영역까지 번지면서 overflow 영역 계산에 영향을 줄 수 있어 Layout까지 일으키는 케이스가 있다. csstriggers 기준으로도 box-shadow는 Layout + Paint + Composite을 모두 트리거한다. paint-only로 외워두지 말고 따로 챙기는 편이 안전했다.