모바일 웹뷰나 요소가 많은 화면에서 요소 이동 애니메이션을 구현할 때 top, left, margin 속성을 조작하면 화면이 버벅거리며 프레임 드랍이 난다. 같은 이동 연산을 transform: translate3d()로 바꾸면 60프레임으로 매끄럽게 작동한다. 이런 성능 차이는 CPU와 GPU의 역할 분담 및 브라우저의 Graphic Layer 구조에서 발생한다.
웹에서 하드웨어 가속과 Graphic Layer

CPU 메인 스레드는 DOM 파싱, 자바스크립트 실행, 레이아웃 위치 계산으로 늘 핑퐁 연산이 몰려있다. GPU 는 단순한 비트맵 병렬 연산에 특화된 수천 개의 코어를 갖고 있다.
브라우저는 특정 CSS 속성이 적용된 렌더 노드를 일반 Render Layer 에서 Graphic Layer(Compositor Layer)로 승격시킨다. GPU VRAM 메모리에 독립적으로 할당해서 빠르게 그래픽을 처리하는 것이다.
Graphic Layer 의 HTML 요소는 브라우저가 화면 전체 픽셀을 매 프레임 다시 그리지 않는다. 승격 시점 시각적 형태를 '스냅샷 비트맵 텍스처' 형태로 GPU VRAM에 올려두고, GPU가 transform 등의 좌표 이동과 opacity 변화만 빠르게 합성한다.
Reflow, Repaint
- Reflow : 렌더 트리 노드들의 정확한 위치, 크기(너비/높이), 마진 등 기하학적 좌표를 계산
- Repaint : 위치가 계산된 노드들에 색상, 배경, 글자 폰트, 테두리, 그림자 등 시각적 픽셀을 그리기
이 두 과정이 렌더링 성능에 중요하다.
Reflow 는 DOM 요소의 크기나 위치가 변경되거나, DOM 구조 자체가 바뀌었을 때 연관된 모든 노드들의 위치와 크기를 연쇄적으로 재계산해서 계산 범위가 크다. 대표적으로 top, left, bottom, right, width, height, margin, padding, border-width, font-size, display, position 등의 속성이 재계산을 불러온다. 노드 추가, 삭제, 순서 변경, 브라우저 Resize, 폰트 다운로드도 레이아웃을 다시 계산해야 함.
자바스크립트로
offsetWidth,offsetHeight,scrollTop,getBoundingClientRect()등 위치/크기 조회 속성을 호출할 때도 브라우저가 최신값을 알려주기 위해 즉시 Reflow를 강제 수행함...!
Repaint 는 Reflow 보다는 적지만, 픽셀 라스터라이제이션(Rasterization) 작업을 CPU 메인 스레드가 수행하므로, 잦은 Repaint 역시 부드러움을 해치고 렉이 걸릴 수 있다. 대표적으로는 color, background-color, border-color, visibility, outline, box-shadow 등의 속성이 있음
Reflow가 발생하면 그 결과로 반드시 Repaint가 따라 발생하는 것 주의! 위치가 바뀌었으니 새로 그려야 해서.
GPU 하드웨어 가속을 유발하는 주요 CSS 속성들
GPU 레이어에서는 Composite 을 사용해서 레이어의 좌표와 투명도 변화 등의 합성만 계산한다. transform, opacity, will-change, filter 등등.
transform
.animated-box {
transform: translate3d(0, 0, 0);
transform: translateX(100px);
}
translate3d(), translateZ(0), scale(), rotate() 같은 변형 속성은 레이아웃 치수를 재계산하지 않고 GPU 텍스처의 매트릭스 변환만 수행한다.
opacity
.fade-element {
opacity: 0.5;
transition: opacity 0.3s ease;
}
독립된 Graphic Layer의 alpha 채널 값만 GPU에서 조작하므로 Reflow나 Repaint 없이 합성 단계만 거친다.
will-change
.modal-overlay {
/* 브라우저에게 애니메이션 예정 속성을 사전에 힌트로 제공 */
will-change: transform, opacity;
}
will-change 속성을 부여하면 브라우저는 애니메이션이 실제 시작되기 전에 해당 노드를 미리 Graphic Layer로 올려둔다. 레이어 승격 시 발생하는 100~200ms의 초기 지연을 예방하는 역할을 한다.
filter 및 backdrop-filter
.blur-card {
filter: blur(10px);
/* Glassmorphism 효과 */
backdrop-filter: blur(16px) saturate(180%);
}
블러, 블렌딩 등의 픽셀 단위 계산 연산은 CPU에 부하가 크므로, GPU 픽셀 셰이더(Pixel Shader) 연산에 위임한다.
position: fixed 및 position: sticky
.header-bar {
position: fixed;
top: 0;
left: 0;
}
스크롤 시 메인 스레드의 Repaint 없이 Compositor Thread에서 부드럽게 뷰포트 고정 스크롤을 처리
mix-blend-mode 및 isolation: isolate
.blend-text {
mix-blend-mode: multiply;
isolation: isolate;
}
요소 간 픽셀 혼합 연산을 위해 상위/하위 요소 블렌딩 셰이더로 처리한다.
Stacking Context 생성 조건과 isolation: isolate를 활용한 독립 레이어 격리는 Stacking Context 의 원리Stacking Context 의 원리z-index: 9999를 부여해도 요소가 부모 레이어 뒤로 깔리는 원인인 쌓임 맥락(Stacking Context)의 생성 조건과 isolation: isolate를 활용한 트러블슈팅 정리. 글에서 다룬다.
overflow: scroll / overflow: auto (Composited Scrolling)
.scroll-container {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
Compositor Thread에서 처리하기 위해 스크롤 영역을 별도 레이어로
contain (contain: paint / strict)
.widget {
contain: paint;
}
자식 요소가 컨테이너 경계 밖을 벗어나지 않음을 보장해 렌더링 스코프를 완전히 격리(Isolate)함
will-change 사용시 주의
will-change 를 남발하면 그거대로 문제가 생긴다.
- GPU VRAM 메모리 과다 점유: Graphic Layer가 생성될 때마다 비트맵 텍스처가 VRAM 를 차지한다. 모바일 기기에서는 메모리 부족으로 크래시 나거나 프레임이 오히려 떨어진다.
- 사전 준비 오버헤드: 수많은 요소를 미리 레이어로 쪼개어두면 초기 페인팅 시 라스터라이제이션(Rasterization) 연산량이 급증해서 기기가 힘들어할 수 있다.
따라서 will-change는 필요할 때 자바스크립트로 동적 부여하고 애니메이션이 끝나면 털어내는 형태로 관리하는 편이 안전하다.
const element = document.querySelector('.animated-target') as HTMLElement
// 애니메이션 시작 직전 부여
element.style.willChange = 'transform'
element.addEventListener('transitionend', () => {
// 애니메이션 종료 후 즉시 제거해 메모리 해제
element.style.willChange = 'auto'
}, { once: true })
Vue3 에서 will-change
Vue3 에서 컴포넌트 Unmount 시 DOM 요소 제거와 함께 자동으로 GPU 메모리가 해제된다. 하지만 수명이 긴 컴포넌트의 애니메이션이 단 0.3초만 필요한 경우면 will-change: auto 로 해제해주는 게 좋다.
Transition
<Transition>은 enter/leave 애니메이션이 진행되는 동안만 v-enter-active, v-leave-active 클래스를 붙이고, 애니메이션이 끝나면 클래스를 제거하거나 DOM을 삭제하기 때문에 클래스에 will-change가 있었다면 자연스럽게 해제된다.
다만, <KeepAlive> 로 감싸진 경우는 남아 있을 수 있다.
<TransitionGroup>에서 리스트 순서가 바뀔 때 부드럽게 이동하는 v-move 기능은 내부적으로 FLIP (First, Last, Invert, Play) 기법을 사용한다. 내부적으로 getBoundingClientRect() 를 호출해서 강제로 동기 Reflow 가 발생해 순간 병목이 올 수 있다.
v-if, v-show
v-if: 변경 시 DOM 트리에 노드를 실제 생성/제거하므로 렌더 트리가 재구성되어 Reflow + Repaint가 발생v-show: DOM 노드는 유지한 채 display 스타일만 전환한다. 하지만 이 역시 레이아웃 크기가 달라져 Reflow + Repaint가 발생
DOM 을 고치기
watch나 onUpdated, nextTick() 내부에서 offsetWidth 같은 DOM 치수를 측정한 뒤 그 값으로 또 반응형 상태를 변경하여 DOM을 다시 고치는 무한 Reflow 루프에 빠질 수 있음
onUpdated(() => {
const height = containerRef.value.offsetHeight // -> Reflow
containerHeight.value = height // reactive state 업데이트 -> onUpdated 트리깅 -> 또 reflow 무한
})
Chrome DevTools에서 Graphic Layer 확인하기
어떤 요소가 GPU Graphic Layer 에 있는지 확인할 수 있는데, Chrome DevTools 에 기능이 있다!
- 크롬 기준으로 개발자 도구를 연다.
Cmd + Shift + P(Mac) 또는Ctrl + Shift + P(Windows)를 눌러 Command Menu를 호출한다.Rendering을 검색해 선택한다.- Layer Borders 옵션을 체크하면 테두리가 보인다. 초록색 테두리가 그래픽 레이어임
댓글 0개