CSS 애니메이션 성능 개선 방법(reflow 최소화, will-change 사용)

Posted by in Research

모바일 네이버 지도 개편을 하면서 검색창 부분과 상세페이지 스크롤 부분에 애니메이션이 추가되었습니다.

초기 검색창 부분 애니메이션은 CSS 속성 중 margin, height을 사용하여 구현했습니다. 하지만 애니메이션이 부자연스러운 현상이 있어 자료를 찾아보니 margin, height 속성은 애니메이션에 사용했을 때 성능 저하를 유발하는 속성 중 하나였습니다. 성능에 영향이 있는 속성들은 페이지 reflow를 일으키는 속성들이였고 이를 수정하여 성능을 향상 시킬 수 있었습니다.

이 글에서는 reflow를 최소화 하는 방법과 will-change를 설명하겠습니다.

검색창 애니메이션

초기에는 아래와 같은 애니메이션을 구현하고자 했습니다.

총 4가지 애니메이션이 들어가있는데요.

  1. 검색창이 왼쪽으로 축소되고 아래쪽으로는 확장됩니다.
  2. 검색창이 왼쪽으로 계속 축소되고 아래쪽으로는 확장됩니다.
    추가로 한 줄 검색창은 fade-out, 두 줄 검색창은 fade-in 되고, 오른쪽 버튼 중 ‘X’ 버튼은 왼쪽으로 90°회전, ‘↑↓’ 버튼은 아래쪽으로 이동합니다.
  3. 위와 동일합니다.
  4. 애니메이션이 종료됩니다.

성능문제

언뜻 보이게는 애니메이션이 잘 적용된 것으로 보입니다. 하지만 여기에 애니메이션으로 사용하면 안되는 CSS 속성이 추가되어 있습니다.

  • 왼쪽으로 축소: margin(성능 문제)
  • 아래쪽으로 천천히 확장: height(성능 문제)
  • fade in / fade-out: opacity
  • ‘X’ 버튼 회전: rotate
  • ‘↑↓’ 버튼 아래쪽으로 이동: translateY

UX 엔지니어이자 자바스크립트 개발자인 윌 보이드(Will Boyd)는 조금 더 부드러운 애니메이션을 만들 수 있는 방법과 그 예시를 소개하였습니다.(참고 영상, 참고 슬라이드)

“브라우저에서 하나의 애니메이션 프레임을 처리한다는 것은 애니메이션 구현에 필요한 모든 계산 과정과 계산을 통해 얻어진 픽셀 자리를 업데이트 하는 것까지 포함합니다. 목표는 브라우저가 이 과정에서 할일을 최대로 줄여서 초당 60프레임 정도의 부드러운 애니메이션을 만드는 것입니다. CSS 애니메이션 구현 비용과 직접적으로 연관되어 있는 것은 재조정(reflow)재색칠(repaint)를 일으키지 않는 속성들입니다.”

즉, 부드러운 애니메이션을 적용하려면 reflowrepaint를 최소화 시켜야 합니다.

reflow란?

생성된 DOM 노드의 레이아웃(너비, 높이 등) 변경 시 영향받는 모든 노드(자식, 부모)의 수치를 다시 계산하여 렌더 트리를 재생성하는 작업입니다.

예) left

해당 엘리먼트를 이동시키기 위해 left값을 사용하면 reflowrepaint 모두 발생합니다.(이동할 때 마다 reflow, repaint 발생)

reflow가 발생하는 속성(참고)

width height
padding margin
display border-width
border top
position font-size
float text-align
overflow-y font-weight
overflow left
font-family line-height
vertical-align right
clear white-space
bottom min-height

repaint란?

reflow 과정이 끝난 후 재생성된 렌더 트리를 다시 그리는 작업으로 수치와 상관없는 background-color, visibility, outline 등의 스타일 변경시에는 reflow 과정이 생략 된 repaint 작업만 수행합니다.

예) background-color

해당 엘리먼트의 배경색을 바꾸기 위해 background-color를 사용하면 repaint가 발생합니다.(색이 바뀔 때 마다 repaint 발생)

repaint가 발생하는 속성(참고)

color border-style
visibility background
text-decoration background-image
background-position background-repeat
outline-color outline
outline-style border-radius
outline-width box-shadow
background-size

reflow를 피하거나 최소화하는 방법

1. 클래스 변화에 따른 스타일 변화를 원할 경우, 최대한 DOM 구조 상 끝단에 위치한 노드에 추가합니다.

클래스가 변화할 때 reflow가 일어나는 것은 피할 수 없지만 성능 문제를 최소화 할 수 있습니다.
아래의 코드는 자바스크립트를 이용하여 <div id="change">의 넓이를 50%로 줄이려고 하는 코드입니다.
코드에는 .addclass 를 추가하여 넓이를 줄이려 하고 있는데 DOM 트리에서 가장 말단에 있는 노드에 클래스를 추가하여 넓이를 줄일 때 가장 빠릅니다.

결과

<div id="first">.addclass를 추가했을 때

<div id="second">.addclass를 추가했을 때

<div id="change">.addclass를 추가했을 때

빠른순서

<div id="change">(1.656ms) >  <div id="second">(1.783ms) >  <div id="first">(17.23ms)

2. 애니메이션이 들어간 엘리먼트는 가급적 position: fixed 또는 position: absolute로 지정합니다.

위치 이동을 구현한 애니메이션(넓이나 높이값 변경 등)은 reflow가 짧은 시간 내 반복적으로 일어나게 됩니다. 그래서 사용하지 않는 것이 가장 바람직하나 반드시 사용해야 한다면 애니메이션이 들어간 요소에 position: absolute 혹은 position: fixed 속성을 적용합니다. 다른 요소에는 영향을 끼치지 않으므로 페이지 전체가 아닌 해당 요소만 reflow가 발생합니다.

위의 그림은 보라색 정사각형(블록1)에 세로 길이가 늘어가는 애니메이션이 추가되어 있습니다.
왼쪽 그림의 경우 보라색 정사각형(블록1)의 세로 길이가 늘어남에 따라 흰색 직사각형(블록2) 위치가 이동하므로 블록1, 블록2 모두 reflow가 발생하고 있습니다.

가운데 그림의 경우 보라색 정사각형(블록1)에 position: absolute 속성이 적용되어 있으므로 세로 길이가 늘어나도 흰색 직사각형(블록2)의 위치는 이동하지 않습니다. 그래서 블록1만 reflow가 발생합니다. 하지만 원하던 결과가 발생하지 않습니다.

오른쪽 그림의 경우 보라색 정사각형(블록1)에 position: absolute 속성이 적용되어 있고 흰색 직사각형(블록)에 transform: translateY() 속성이 적용되어 있습니다. 그러므로 블록1만 reflow가 발생하고 원하는 결과가 나타납니다.

3. JS를 통해 스타일변화를 주어야 할 경우, 가급적 한번에 처리합니다.

style 객체를 여러번 호출해 적용한 코드

CSS에 정의된 class를 통해 한번에 적용한 코드

자바스크립트에서 style을 여러번 호출하는 것(7.7ms) 보다 클래스를 통하여 스타일 변화(5.3ms)를 처리하는 것이 랜더링 속도가 더 빠릅니다.

4. 인라인 스타일을 최대한 배제합니다.

위의 내용과 중복되며, reflow 비용을 줄이는 것 이외에 코드 가독성도 높일 수 있습니다.

5. 테이블 레이아웃을 피하는 것이 좋습니다.

테이블 레이아웃을 사용하게 되면 테이블 값에 따라 넓이를 계산하므로 랜더링이 느려집니다. 그러므로 꼭 필요한 경우를 제외하고는 테이블 레이아웃을 사용하지 않는 것이 좋습니다. 만약 사용한다면 CSS 속성 table-layout:fixed를 사용하면 랜더링을 조금 더 빠르게 할 수 있습니다.

10×10 테이블

table-layout: fixed 미 적용

table-layout: fixed 적용

table-layout: fixed 미 적용(0.6ms) <   table-layout: fixed 적용(0.4ms)

100×100 테이블

table-layout: fixed 미 적용

table-layout: fixed 적용

table-layout:fixed 미 적용(35.4ms) < table-layout:fixed 적용(27.1ms)

6. CSS 하위선택자는 필요한 만큼 정리하는 것이 좋습니다.

reflow 자체보다는 reflow가 유발시키는 CSS Recalculation에 필요한 내용입니다. CSS 규칙은 오른쪽에서 왼쪽으로 이동합니다. 이 과정에서는 더 이상 일치하는 규칙이 없거나 잘못된 규칙이 나올 때 까지 계속됩니다. 그러므로 불필요한 선택자를 사용하는 것은 성능을 저하시킬 수 있습니다.

7. 기타방법

아래의 방법은 아직 테스트를 완료하지 못했거나 테스트 결과가 기대했던 결과와 일치하지 않는 방법입니다.
추후 테스트를 완료하는데로 수정하여 올리도록 하겠습니다.

  • 테스트 예정
    • IE의 경우 CSS에서의 js 표현식을 피하라
    • 캐쉬를 활용한 reflow 최소화
    • DOM 사용을 최소화하여 reflow 비용 줄이기
  • 테스트 결과 불확실
    • position: relative와 top, left 값을 함께 사용 시 주의하자(테스트 결과 불일치)

참고: reflow 과정 최적화, reflow 원인과 마크업 최적화 tip

will-change 검토

will-change란?

will-change는 변화가 예상되는 요소를 브라우저에게 미리 알려줍니다. 브라우저는 실제 요소가 변화되기 전에 적절하게 최적화를 할 수 있습니다. 큰 비용이 드는 변화도 최적화로 인해 페이지의 반응성을 증가시킬 수 있습니다.

will-change의 지원범위

will-change 속성

will-change 속성은 4가지가 있습니다.

  • auto
    • 기본값으로 브라우저는 별다른 최적화를 실시하지 않습니다.
  • scroll-position
    • 스크롤 할 때 엘리먼트의 위치가 변경될 것을 알려줍니다. 이 값을 설정하면 브라우저는 스크롤 가능한 엘리먼트를 미리 최적화 하여 랜더링 합니다. 한 번에 많은 양을 스크롤하거나 빠른 스크롤이 필요한 경우에 사용합니다.
  • contents
    • 엘리먼트의 컨텐츠가 변경될 것을 알려줍니다. 브라우저는 보통 엘리먼트의 랜더링 결과를 캐싱합니다. 대부분의 엘리먼트가 변경되지 않고 변경되어도 위치가 바뀌는 정도의 미미한 변경만 발생하기 때문입니다. 하지만 엘리먼트가 계속해서 변경되는 경우 브라우저 캐시는 무의미하게 됩니다. 이 속성을 사용하게 되면 캐시를 하지 않고 변경될 때마다 처음부터 랜더링하게 됩니다.
  • <custom-ident>
    • 변경하고 싶은 속성을 사용할 수 있습니다. 쉼표(,)를 이용하여 두 개 이상의 속성을 사용할 수 있습니다. 크롬에서는 현재 6가지 속성(opacity, transform, top, left, right, bottom)만 적용됩니다. 참고.
will-change 속성을 사용하면 해당 레이어는 GPU에 업로드 됩니다.
아래의 그림은 빨간색 네모에 마우스를 올리면 will-change 속성이 적용되도록 한 그림입니다. 크롬개발자도구 Layer창에서 보면 GPU에 업로드 시 색깔이 변하는 것을 확인할 수 있습니다.

will-change 사용시 주의할 점

1. 너무 많은 속성과 요소에 will-change 속성을 사용하지 않습니다.

브라우저는 모든 요소에 대해 이미 최적화를 시키려고 시도하고 있습니다. will-change 속성이 사용된 요소는 최적화를 하기 위해 많은 자원을 소모하기 때문입니다.

이 코드는 브라우저가 모든 요소에 대해 최적화를 하여 성능이 크게 좋아질 것으로 생각할 수 도 있지만 실제로 효과가 전혀 없을 수도 있고 성능이 더 안 좋아질 수도 있습니다.

will-change에 모든 속성을 사용했을 때:

will-change를 사용하지 않았을 때:

2. 애니메이션 동작이 끝난 후 기본 상태로 되돌려야 합니다.

브라우저가 변화에 최적화를 시도하면 일반적으로 비용이 발생합니다. 브라우저는 보통 필요한 경우에 최적화를 실시하고 최적화가 필요가 없으면 다시 원래되로 되돌아 옵니다. 하지만 will-change의 경우는 최적화를 길게 유지하게 됩니다. 그러므로 엘리먼트에 변경이 종료되면 반드시 will-change를 삭제해야 합니다. 그러면 will-change에 사용하고 있던 자원을 회수할 수 있습니다.

단, 슬라이드처럼 수초 내에 반드시 변화가 일어나거나 마우스 움직임에 따라 변화가 일어나는 경우는 자바스크립트로 will-change로 삭제하지 않고 스타일시트에 바로 선언해도 문제 없습니다.

3. 조금 더 빨리 적용하려고 will-change를 사용해서는 안 됩니다.

will-change를 사용하지 않아도 페이지가 잘 작동된다면 will-change를 사용하지 않아도 됩니다. 조금 더 빨리하기 위해 will-change 속성을 추가하면 과도한 메모리 사용과 더 복잡한 렌더링으로 성능이 더 안좋아 질 수 있습니다.

top, left로 이동하는 물고기 100마리

will-change 적용한 top, left로 이동하는 물고기 100마리

4. 브라우저에게 미리 will-change 사용을 알려주어야 합니다.

변화가 일어날 요소에 will-change를 직접 선언하면 적용이 되지 않습니다.

그러므로 선택자 :hover, 자바스크립트 mouseenter 등을 통하여 미리 알려주어야 합니다.

성능 개선

1. transform: translateY() + position: absolute 이용?

아래의 애니메이션 처럼 1번과 2번 영역을 나누고 2번 영역이 1번을 덮는 방식으로 진행하면 reflow가 일어나지 않는 속성만을 이용하여 해결할 수 있습니다.

하지만 스크롤 애니메이션 때문에 현재 마크업 구조가 아래의 그림 처럼 나누어져 있어서 적용할 수 없었습니다.

2. will-change 이용?

will-changewidth, margin의 변화를 브라우저에 알리고 싶지만 지원이 되지 않습니다.(적용되는 속성: top, left, right, bottom, opacity, transform)
will-change: width, margin 대신에 will-change:transform 을 적용하면 해당 레이어가 GPU에 업로드 되기는 합니다. 하지만 width, margin 속성 자체가 reflow를 계속해서 발생 시키기 때문에 좋은 방법은 아닙니다.

width변경 속성에 will-change: width 적용 했을 때는 적용하지 않았을 경우와 동일합니다.

width변경 속성에 will-change: transform 적용 했을 때는 GPU 영역에 레이어가 업로드 되나 reflow가 계속 발생합니다.

그리고 지원범위 또한 안드로이드 5이상, IOS 9.3 이상 버전에서만 적용되기 때문에 적용하지 않았습니다.

3. fade in/out 이용!

앞서 말한 속성 5가지 중에 성능에 좋지않은 margin 속성을 사용하지 않았습니다. height 속성은 사용할 수 밖에 없었지만 한번에 확장하도록 바꿈으로써 reflow를 줄였습니다.

  • 왼쪽으로 축소: margin(성능 문제) → 제거
  • 아래쪽으로 천천히 확장: height(성능 문제) → 아래쪽으로 한 번에 확장
  • Fade in / Fade-out: opacity
  • ‘X’ 버튼 회전: rotate
  • ↑↓’ 버튼 아래쪽으로 이동: translateY

초기 애니메이션보다 현재 애니메이션에서 리플로우가 적게 일어나는 것을 알 수 있습니다.

초기 애니메이션

현재 애니메이션


참고: 현재 지도 애니메이션

결론

초기에 구현하고자 했던 애니메이션은 reflow가 많이 일어나서 성능에 좋지 않았습니다. 그래서 성능 문제를 해결하고자 여러가지 방법을 찾아보았습니다.

만약 구조를 바꿀 수 있었다면 translateY + position: absolute 방법을 이용할 수 있었고, 대응 브라우저 버전이 높았다면 will-change 속성을 사용해서 해결할 수 있었습니다.

하지만 두 방법 모두 적용할 수가 없어서 네이버 지도에서는 fade in/out 애니메이션으로 수정되었으며, 애니메이션 수정으로 reflow 수가 줄어들어 성능을 향상 시킬 수 있었습니다. 성능을 고려한 CSS 애니메이션 개발에 본 글이 도움이 되길 바랍니다.

참고자료