서론
애니메이션 성능 최적화 기법은 세상에 넘쳐납니다. 예를 들어:
-
transform,opacity만 변경하고 다른 속성은 건드리지 않기 (리플로우 방지) -
애니메이션 요소에
transform: translate3d(0, 0, 0),will-change: transform등을 적용하여 하드웨어 가속 활성화 -
애니메이션 요소를
position: fixed,absolute로 배치하여 리플로우 방지 -
애니메이션 요소에 높은
z-index적용하여 컴포지트 레이어 수 줄이기 -
기타 유용한 규칙
문제는: 이러한 규칙을 조심스럽게 지키고 있는데도 왜 애니메이션은 여전히 끊기고 프레임 드랍이 발생할까? 더 최적화할 수 있을까? 어디서 시작해야 할까?
일.하드웨어 가속은 비표준적이다
The most important thing I'd like to tell you before we dive deep into GPU compositing is this: It's a giant hack. You won't find anything (at least for now) in the W3C's specifications about how compositing works, about how to explicitly put an element on a compositing layer or even about compositing itself. It's just an optimization that the browser applies to perform certain tasks and that each browser vendor implements in its own way.
대부분의 경우 하드웨어 가속을 활성화하면 확실히 성능이 향상되지만, 이 부분은 비표준적입니다. W3C 에는 컴포지팅이 어떻게 작동하는지, 요소를 컴포지트 레이어에 명시적으로 배치하는 방법, 혹은 컴포지팅 자체에 대한 명세가 없습니다. 이는 브라우저가 특정 작업을 수행하기 위해 적용하는 최적화이며, 각 브라우저 벤더가 독자적으로 구현하는 것입니다.
즉, transform: translate3d(0, 0, 0) 등의 기법으로 하드웨어 가속을 활성화하는 것은 명세 외의 행위이며, 성능이 향상될 수도 있지만 심각한 성능 문제를 초래할 수도 있습니다
장래에 명세가 정비될 수도 있겠지만, 그 전까지는 각 항의 성능 최적화 원칙을 준수할 뿐만 아니라, 실제 렌더링 플로우를 고려하여 원리에서 성능 문제를 해결해야 합니다.
하드웨어 가속 (Hardware Acceleration)
CSS 애니메이션における 하드웨어 가속이란 GPU 컴포지팅 (GPU compositing) 을 말하며, 브라우저가 CPU 로 직접 이미지 데이터를 생성하여 표시하는 것이 아니라 관련 레이어 데이터를 GPU 에 전송하고, GPU 는 이미지 데이터 연산에 능숙하므로 가속으로 간주됩니다.
그렇다면 하드웨어 가속을 사용할 수 없을 때 브라우저는 어떻게 페이지를 렌더링할까?
在没有硬件加速的情况下,浏览器通常是依赖于 CPU 来渲染生成网页的内容,大致的做法是遍历这些层,然后按照顺序把这些层的内容依次绘制在一个内部存储空间上(例如 bitmap),最后把这个内部表示显示出来,这种做法就是软件渲染(software rendering)
이.transform 과 opacity 의 특수성
예전에는 레이아웃 관련 속성을 변경하여 애니메이션을 만들었습니다. 예를 들어:
@keyframes move {
from { left: 30px; }
to { left: 100px; }
}
애니메이션의 각 프레임마다 브라우저는 요소의 모양과 위치를 다시 계산하고 (리플로우), 새로운 상태를 렌더링하고 (리페인트), 화면에 표시합니다.
페이지 전체의 리플로우와 리페인트는 느리지만, 애니메이션 요소를 전경으로 추출하고 각 프레임에서 다른 부분을 배경으로 변경하지 않고 애니메이션 요소만 다시 렌더링한 후 전경과 배경을 합성하면 더 빨라지지 않을까? 확실히, GPU 는 서브픽셀 레벨의 레이어 합성을 빠르게 수행할 수 있기 때문입니다.
하지만 이 전제는 움직이는 부분과 움직이지 않는 부분으로 전경과 배경 레이어를 나눌 수 있어야 합니다. 애니메이션 요소가 레이아웃의 영향을 받거나 움직임 중에 레이아웃에 영향을 주면 전경과 배경의 경계가 무너지고 단순히 2 개의 레이어로 나눌 수 없게 됩니다. 그렇다면 position: fixed | absolute 를 적용하면 레이아웃에 영향을 주지 않는다는 것을 보장할 수 있을까?
아닙니다. left 는 퍼센티지 값이나 상대 단위 (em, vw 등) 를 받을 수 있기 때문에, 브라우저는 해당 속성의 변화가 레이아웃과 무관하다는 것을 100% 확신할 수 없어 단순히 전경과 배경 레이어를 나눌 수 없습니다. 예를 들어:
@keyframes move {
from { left: 30px; }
to { left: 100%; }
}
하지만 브라우저는 transform 과 opacity 의 변화가 레이아웃과 무관하다는 것을 100% 확신할 수 있고, 레이아웃의 영향을 받지 않으며, 그 변화가 기존 레이아웃에 영향을 주지도 않습니다. 따라서 이 두 속성의특수성은 다음과 같습니다:
-
does not affect the document's flow,
-
does not depend on the document's flow,
-
does not cause a repaint.
레이아웃에 영향을 주지도 않고 레이아웃의 영향을 받지도 않으며, 그 변화가 다른 부분의 리페인트를 일으키지 않는다면, 그것은 확실히 독립된 레이��로 추출되어 GPU 에 안심하고 처리하게 할 수 있으며 하드웨어 가속의 이점을 누릴 수 있습니다:
-
섬세함 (GPU 는 서브픽셀 레벨의 정밀도를 실현할 수 있고 GPU 에게 부담이 되지 않음)
-
부드러움 (계산 집약적인 JS 작업의 영향을 받지 않고 애니메이션은 GPU 에게 위임되며 CPU 와 무관함)
삼.GPU 컴포지팅의 대가
It might surprise you, but the GPU is a separate computer. That's right: An essential part of every modern device is actually a standalone unit with its own processors and its own memory- and data-processing models. And the browser, like any other app or game, has to talk with the GPU as it would with an external device.
GPU 는 독립된 부분이며 자체 프로세서, 메모리, 데이터 처리 모델을 가지고 있습니다. 즉, CPU 에서 메모리 내에 생성된 이미지 데이터를 GPU 와 직접 공유할 수 없고, 패키징하여 GPU 에 전송해야 하며, GPU 는 이를 받아서야 우리가 기대하는 일련의 작업을 실행할 수 있습니다. 이 과정에는시간이 소요되고 데이터 패키징에는메모리가 필요합니다.
필요한 메모리 양은 다음에 따라 달라집니다:
-
컴포지트 레이어의 수
-
컴포지트 레이어의 크기
수보다 컴포지트 레이어의 크기의 영향이 더 큽니다. 예를 들어:
.rect {
width: 320px;
height: 240px;
background: #f00;
}
이 빨간 블록을 GPU 에 전송하는 경우 필요한 저장 공간은: 320 × 240 × 3 = 230400B = 225KB(RGB 는 3 바이트 필요). 이미지에 투명 부분이 있는 경우 320 × 240 × 4 = 307200B = 300KB 가 필요합니다.
이렇게 눈에 띄지 않는 작은 빨간 블록도 2,300KB 가 필요하며, 페이지에는 수십에서 수백 개의 요소가 있고 화면의 절반이나 전체를 차지하는 요소도 적지 않습니다. 이 모두를 컴포지트 레이어로 GPU 에 위임하면 메모리 소비는 상상하기 어렵지 않습니다. 따라서 일부 극단적인 하드웨어 가속 시나리오에서는 성능이 매우 나쁩니다:
[caption id="attachment_1251" align="alignnone" width="303"]
gpu compositing issue[/caption]
1GB RAM 의 기기의 경우 시스템과 백그라운드 프로세스에서 1/3, 브라우저와 현재 페이지에서 1/3 이 소비되고 실제로 사용 가능한 것은 200~300MB 뿐입니다. 컴포지트 레이어가 너무 많거나 크기가 너무 크면, 메모리가 급속히 소비되고 프레임 드랍 (끊김, 깜빡임) 이 발생하며, 경우에 따라서는 브라우저/애플리케이션이 충돌할 수도 있습니다.
P.S.자세한 내용은 CSS3 硬件加速也有坑!!! 참조
사.컴포지트 레이어 생성
브라우저는 몇 가지 상황에서 컴포지트 레이어를 생성합니다. 예를 들어:
-
3D transforms: translate3d, translateZ and so on;
-
<video>, <canvas> and <iframe> elements;
-
animation of transform and opacity via Element.animate();
-
animation of transform and opacity via СSS transitions and animations;
-
position: fixed;
-
will-change;
-
filter;
-
。。。
다른 것도 많습니다. 자세한 내용은 CompositingReasons.h 에 정의된 상수 참조
이들 대부분은 기대하는 대로 명시적으로 생성된 컴포지트 레이어이지만, 다른 상황에서도 컴포지트 레이어가 생성됩니다:
- 컴포지트 레이어 위에 있는 요소도 컴포지트 레이어로 생성됨 (B 의
z-index가 A 보다 크고 A 에 애니메이션을 적용하면 B 도 독립된 컴포지트 레이어에 밀어넣어짐)
이해하기 쉽습니다. 애니메이션 과정에서 A 와 B 가 겹칠 가능성이 있고 B 에 가려질 수 있으므로, GPU 는 각 프레임에서 A 레이어에 애니메이션을 적용한 후 B 레이어와 합성하여 올바른 결과를 얻어야 합니다. 따라서 B 는 어떻게든 컴포지트 레이어에 밀어넣어지고 A 와 함께 GPU 에 위임되어야 합니다.
암시적으로 컴포지트 레이어가 생성되는 것은 주로 겹침을 고려한 것입니다. 브라우저가겹칠지 말지 확신할 수 없는경우, 불확실한 것을 모두 컴포지트 레이어에 밀어넣어야 합니다. 따라서 이 관점에서 보면 높은 z-index 라는 원칙에는 일리가 있습니다.
오.하드웨어 가속의 장단점
장점
-
애니메이션이 매우 부드러우며 60fps 에 도달
-
애니메이션 실행 프로세스는 독립된 스레드 내에서 이루어지며 계산 집약적인 JS 작업의 영향을 받지 않음
단점
-
요소를 컴포지트 레이어에 밀어넣을 때 추가 리페인트가 필요하며, 경우에 따라 느림 (페이지 전체 리페인트가 필요할 수도 있음)
-
컴포지트 레이어의 데이터를 GPU 에 전송할 때 추가 시간이 소요되며, 컴포지트 레이어의 수와 크기에 따라 다름. 이로 인해 미들~로우엔드 기기에서깜빡임이 발생할 수 있음
-
각 컴포지트 레이어는 일부 메모리를 소비하며, 모바일 기기에서는 메모리가 비싸고 과도한 점유는 브라우저/애플리케이션의충돌을 초래할 수 있음
-
암시적인 컴포지트 레이어의 문제가 있으며, 주의하지 않으면 메모리가 급증함
-
글자가 흐려지거나 요소가 변형될 수 있음
가장 중요한 문제는메모리 소비와리페인트에 집중되어 있으므로, 애니메이션 성능 최적화의 목표는 메모리 소비를 줄이고 리페인트를 줄이는 것입니다.
육.성능 최적화 기법
1.암시적인 컴포지트 레이어 피하기
컴포지트 레이어는 리페인트와 메모리 소비에 직접적인 영향을 미칩니다: 애니메이션 시작 시 컴포지트 레이어를 생성하고 종료 시 삭제하면 리페인트가 발생합니다. 애니메이션 시작 시 레이어 데이터를 GPU 에 전송해야 하므로 메모리 소비는 여기에 집중됩니다. 두 가지 제안:
-
애니메이션 요소에 높은
z-index를 적용하고body의 자식 요소로 직접 배치합니다. 깊게 중첩된 애니메이션 요소의 경우body下に 복사본을 생성하여 애니메이션 효과 구현에만 사용합니다. -
애니메이션 요소에
will-change를 적용합니다. 브라우저는 이러한 요소를 미리 컴포지트 레이어에 밀어넣어 애니메이션 시작/종료 시 더 부드럽게 할 수 있지만, 남용해서는 안 됩니다. 불필요해지면 바로 제거하여 메모리 소비를 줄입니다.
2.transform 과 opacity 만 변경하기
transform, opacity 를 사용할 수 있으면 우선적으로 사용하고, 사용할 수 없으면 방법을 찾아서 사용합니다. 예를 들어 배경색 그라데이션은 위에 있는 의사 요소의 배경색 opacity 애니메이션으로 시뮬레이션할 수 있습니다. box-shadow 애니메이션은 아래에 있는 의사 요소의 opacity 애니메이션으로 시뮬레이션할 수 있습니다. 이러한 곡괭이 구현 방식은 현저한 성능 향상을 가져옵니다.
3.컴포지트 레이어의 크기 줄이기
작은 요소를 확대 표시하는 경우 width, height 를 줄이고 GPU 에 전달하는 데이터를 줄이며, GPU 에서 scale 하여 확대 표시합니다. 시각적인 차이는 없습니다 (단색 배경 요소에 많이 사용되며, 중요도가 낮은 이미지도 5% 에서 10% 의 너비와 높이 압축이 가능합니다). 예를 들어:
<div id="a"></div>
<div id="b"></div>
<style>
#a, #b {
will-change: transform;
background-color: #f00;
}
#a {
width: 100px;
height: 100px;
}
#b {
width: 10px;
height: 10px;
transform: scale(10);
}
</style>
최종적으로 표시되는 두 개의 빨간 블록은 시각적으로 차이는 없지만 메모리 소비를 90% 절감할 수 있습니다.
4.자식 요소의 애니메이션과 컨테이너의 애니메이션 고려하기
컨테이너의 애니메이션에는 불필요한 메모리 소비가 존재할 수 있습니다. 예를 들어 자식 요소 사이의 간격도 GPU 에 유효한 데이터로 전송됩니다. 각 자식 요소에 개별적으로 애니메이션을 적용하면 이 부분의 메모리 소비를 피할 수 있습니다.
예를 들어 12 개의 태양 광선이 회전하는 경우, 컨테이너를 회전시키면 컨테이너의 이미지 전체를 GPU 에 전송하지만, 12 개의 광선을 개별적으로 회전시키면 광선 사이의 11 개의 간격이 없어져 메모리를 절반 절약할 수 있습니다.
5.컴포지트 레이어의 수와 크기를 일찍부터 주목하기
처음부터 컴포지트 레이어, 특히 암시적으로 생성된 컴포지트 레이어에 주목하여 나중에 최적화가 레이아웃에 영향을 주는 것을 피합니다.
컴포지트 레이어의 크기는 수보다 영향이 크지만, 브라우저는 몇 개의 컴포지트 레이어를 하나로 통합하는 최적화 작업을 수행합니다. 이를Layer Squashing이라고 합니다. 그러나 경우에 따라서는 하나의 큰 컴포지트 레이어가 몇 개의 작은 컴포지트 레이어보다 더 많은 메모리를 소비하므로, 필요에 따라 이 최적화를 수동으로 비활성화할 수 있습니다:
// 각 요소에 다른 translateZ 적용
translateZ(0.0001px), translateZ(0.0002px)
6.하드웨어 가속을 남용하지 않기
아무 문제도 없는데 transform: translateZ(0), will-change: transform 등의 하드웨어 가속을 강제로 활성화하는 속성을 함부로 추가하지 않습니다. GPU 컴포지팅에는 단점과 부족이 있으며 비표준적인 행위입니다. 최선의 경우에는 현저한 성능 향상을 가져오고, 최악의 경우에는 브라우저를 충돌시킬 수 있습니다.
아직 댓글이 없습니다