애니메이션에서 불필요한 속성 하나가 브라우저 렌더링 성능에 끼치는 영향

황윤서·2021년 8월 7일
19

돈이 떨어지는 애니메이션을 구현하고 나서...

최근 프로젝트에서 위처럼 돈이 나풀거리며 떨어지는 애니메이션을 구현했습니다. '가계부'라는 프로젝트 주제에 맞게 돈을 많이 벌었으면 하는 마음에 돈이 날라다니는 애니메이션을 추가했는 데 프로젝트를 배포하고나서 결과물을 확인하신 멘토님께 이런 피드백을 받았습니다.

제 화면에서는 너무 렉이 많이 걸리네요. 확인해보니까 돈이 떨어지는 애니메이션에서 문제가 있는 것 같아요.

라면서 돈이 떨어지는 애니메이션 상에서 발생하는 문제점을 짚어주셨습니다. Github에 Open되어 있는 어떤 Repo를 참고하면서 keyframe 애니메이션 코드를 가져오면서 잘 동작되니까 문제없겠지 하던 것이 문제를 발생시킨 것이었습니다. Open되어 있는 코드를 가져오는 과정에서 Markup과 Style을 우리 프로젝트에 맞게 수정했고 그 과정에서 keyframe 애니메이션 코드에 불필요한 속성이 남아있었습니다. 바로 'border-radius' 관련된 속성이었습니다.

@keyframes money {
  0% {
    transform: rotate(-180deg) translate(0px, 0px) rotateX(-360deg)
      rotateY(-180deg);
    border-bottom-left-radius: 0px;
    border-top-left-radius: 0px;
  }
  10% {
    border-bottom-right-radius: 0px;
    border-top-right-radius: 3px;
  }
  15% {
    border-bottom-right-radius: 35px;
    border-bottom-left-radius: 0px;
    border-top-right-radius: 60px;
  }
  70% {
    transform: rotate(100deg) translate(10px, 95px) rotateX(180deg)
      rotateY(180deg);
    border-bottom-left-radius: 50px;
  }
  60% {
    border-top-left-radius: 60px;
    border-top-right-radius: 3px;
  }
  70% {
    border-top-left-radius: 10px;
  }
}

위 css 코드가 바로 돈이 나풀거리는 부분에 대한 애니메이션 코드인데 동작되는 것을 확인해보면 border에 대한 변화는 전혀 일어나지 않고 있습니다. 그 이유는 참고한 레퍼런스와 달리 이미지의 크기가 요소의 크기보다 작았고 border-radius가 요소에 대해서 변화를 발생시키고 있기 때문에 사용자에게는 그 변화가 보이지 않은 것이었습니다. 즉, 우리의 결과물에 대해서 border-radius란 필요없는 속성이었던 것이죠. 아래와 같이 돈이 나풀거리면서 사용자에게는 모르게 border-radius가 변화하고 있었습니다.

얼마나 영향을 끼치고 있을까?

프로젝트를 하는 시간은 매우 짧고 많은 기능들을 개발해야 합니다. 그 과정에서 사용자가 인지하지 못할 만큼의 사소한 문제를 해결하는 데 시간을 많이 쏟기보다는 사용자에게 더 큰 영향을 끼칠 수 있는 중요한 기능에 시간을 쏟는 게 효율적이죠. 비록 프로젝트는 끝났지만 해당 문제가 우리 프로젝트와 사용자에게 끼치는 영향을 확인해야만 다음 프로젝트의 시간 계획을 더 정확하게 작성할 수 있기 때문에 프로젝트에서 돈 애니메이션하는 부분만 별도로 실행해서 '불필요한 border-radius'가 끼친 영향을 확인하기로 했습니다.

불필요한 것이 존재한다는 것이 기능상, UI상으로 문제가 없는 상황이라면 문제가 있는 곳은 성능일 가능성이 높겠죠. 멘토님께서 말씀하신 '렉'에 대해서도 결과적으로는 브라우저 렌더링의 '성능'에 대한 문제일테니까요. 그래서 수정하기 전의 '성능'과 border-radius 속성을 제거한 수정 후의 '성능'을 브라우저 개발자 도구의 performance 탭에서 Mac 기준 '⌘ + ⇧ + E'로 실행할 수 있는 'Start profiling and reload page'를 사용해서 측정해보았습니다. 프로젝트에 적용된 것과 마찬가지로 15개의 money 애니메이션이 적용된 요소가 있는 것을 기준으로 측정하였습니다.

수정 전의 성능

총 두 번을 측정했습니다.

  • 평균 Loading 시간 : 3.5 ms
  • 평균 Scripting 시간 : 60 ms
  • 평균 Rendering 시간 : 251.5 ms
  • 평균 Painting 시간 : 139 ms

측정 1의 Idle, System 시간을 제외한 작업 시간 : 485 ms
측정 2의 Idle, System 시간을 제외한 작업 시간 : 423 ms
측정 1, 측정 2의 Idle, System 시간을 제외한 평균 작업 시간 : 454 ms

수정 후의 성능

@keyframes money {
  0% {
    transform: rotate(-180deg) translate(0px, 0px) rotateX(-360deg)
      rotateY(-180deg);
  }
  100% {
    transform: rotate(100deg) translate(10px, 95px) rotateX(180deg)
      rotateY(180deg);
  }
}

money 애니메이션 keyframe 코드에서 불필요한 border-radius 관련 속성을 제거하고 측정한 결과입니다. 마찬가지로 총 두 번 측정했습니다.

  • 평균 Loading 시간 : 3 ms
  • 평균 Scripting 시간 : 52 ms
  • 평균 Rendering 시간 : 193 ms
  • 평균 Painting 시간 : 28 ms

측정 1의 Idle, System 시간을 제외한 작업 시간 : 275 ms
측정 2의 Idle, System 시간을 제외한 작업 시간 : 277 ms
측정 1, 측정 2의 Idle 시간을 제외한 평균 작업 시간 : 276 ms

측정 결과

결과적으로 눈에 띄게 성능이 향상된 것을 확인할 수 있었습니다. 평균 Painting 시간은 무려 111 ms 넘게 줄어 들었으며 평균 Rendering 시간은 53 ms 가 줄어 들었습니다. Idle, System 시간을 제외한 평균 작업 시간은 수정 전에 비해 178 ms, 39.3%의 향상이 이뤄졌습니다. money 애니메이션이 적용된 요소가 15개에 불과함에도 지연되는 시간이 178ms 나 된다는 것은 개수가 늘어나면 늘어날 수록 지연이 눈에 띄게 발생한다는 것이고 네트워크 상황에 따라 특정 사용자에게는 지금 15개라는 적은 개수조차 커다란 지연, 렉을 발생시킬 가능성이 높다는 것입니다. 특히 39.3%라는 숫자를 보면 반드시 수정을 해야 하는 부분이었다는 걸 알 수 있습니다.

그렇지만 위의 결과에서 시간 단위가 작다보니 오차가 있을 수 있고 다른 요인에 의한 부분도 있을 수 있을 것입니다. 그렇기에 더 정확한 성능 향상 확인을 위해 money 애니메이션이 적용된 요소의 개수를 늘려서 측정해서 비교해보았습니다. 약 500개의 요소를 생성하고 측정을 했습니다.

개수를 늘려서 측정하니 평균 Loading 시간과 평균 Script 시간은 이전 결과와 비슷하게 수정 전과 수정 후가 비슷했지만 평균 Rendering 시간은 수정 전보다 수정 후가 더 높은 경향을 드러냈고 평균 Painting 시간은 확연하게 수정 후에는 거의 비중을 차지하고 있지 않을만큼인 것을 확인할 수 있었습니다. 평균 작업 시간은 수정 후가 수정 전에 비해 795ms, 22.2% 의 향상을 이뤘습니다.

프로젝트와 분리된 환경에서 측정했기에 차이는 있겠지만, 결과적으로 보았을 때 저희팀은 프로젝트를 할 때 약 20% 나 되는 성능 향상의 기회를 놓쳤고 사용자에게 20% 나 느린 서비스를 제공했다고 볼 수 있습니다. 1%, 2%의 성능 향상도 큰 서비스의 입장에선 소중할텐데 무려 20%나 되는 성능 향상의 기회가 있었음에도 놓쳤다는 것은 큰 실수라 할 수 있습니다.

더 확신을 얻기 위해 500개로 늘려 테스트한 것을 LightHouse를 통해 Performance 관련 Report를 받았습니다. 그 결과 수정 후는 수정 전에 비해 65점에서 99점으로 21.6% 향상 되었다는 위에서 계산한 결과와 거의 유사한 결과를 얻었습니다. 더 세부적으로 살펴보면 Speed Index, Time to Interactive, Cumulative Layout Shift 항목에서 획기적으로 개선된 것을 확인할 수 있습니다.

왜 이런 결과가 나왔을까?

Google Developers의 자료를 살펴보면 Pixel Pipeline은 JS/CSS > Style > Layout > Paint > Composite 로 구성됩니다.

Style 단계에서는 .head와 같이 선택자에 따라 어떤 스타일 규칙을 어떤 요소에 적용할 지를 계산하며 Layout 단계에서는 화면에서 얼마만큼의 공간을 차지하고 어디에 배치되는 지를 계산하기 시작합니다. Layout 단계에서 중요한 것은 한 요소가 다른 요소에 영향을 줄 수 있기 때문에 계산 시간이 오래 걸리게 되고 브라우저 렌더링 단계에서 상당한 영향을 끼치게 됩니다. Paint 단계는 레이어라 불리는 여러 곳에서 실행되며 계산된 레이아웃에 pixel을 채우는 단계입니다. 그리고 Paint 단계가 여러 레이어에서 그려졌기에 이것을 합치는 작업이 필요한 데 이 단계가 Composite 단계입니다.

여기서 우리의 수정버전이 획기적으로 개선된 이유는 위의 단계 중 Paint 단계를 줄일 수 있었기 때문입니다. CSS 속성들 중에는 요소의 위치 등 레이아웃 등을 변경해서 Layout부터 계산해서 Reflow, Repaint가 발생하는 속성들이 있고, 레이아웃에 영향을 주지 않는 배경 이미지, 텍스트 색상과 같이 Repaint만 발생시키는 속성들이 있습니다. 그리고 Reflow와 Repaint조차 일으키지 않는 transform, opacity와 같은 속성들이 있습니다.

어느 요소에 대해 transform과 opacity와 같은 속성들을 사용하게 되면 해당 속성에 대해서는 Reflow와 Repaint 작업이 일어나지 않는 데 그 이유는 별도의 레이어로 작업되기 때문입니다. 별도의 레이어로 기존 요소들과는 분리되어 연산되기 때문에 Layout 작업이 필요가 없고 그렇기에 바로 Composite 단계로 건너뛸 수 있습니다.

수정 전의 money 애니메이션 코드에서 사용되었던 border-radius 속성은 Repaint 단계를 발생시키는 속성입니다. 그렇기 때문에 애니메이션에서 사용하게 되면 매 프레임마다 Paint 단계를 수행하게 됩니다. 이러한 Paint 단계는 Pixel Pipeline에서 '종종 가장 비용이 많이 드는 단계'라고 위 자료에서 이야기하고 있기에 매 프레임마다 브라우저 렌더링에 있어 많은 비용을 소모하고 있었던 겁니다.

500개로 늘려 측정한 아래 그림을 보면 수정 전에는 매 프레임마다 Paint 작업에 상당한 시간을 소요하고 있다는 것을 확인할 수 있습니다.

반면 수정 후에는 Paint 작업의 비중은 거의 없죠. 이러한 차이가 성능에 커다란 차이를 불러온 것입니다.

결론

애니메이션을 작업할 때는 속성 하나하나를 신경써서 작업해야 합니다. 각 속성이 Reflow를 발생시키는 지 Repaint만을 발생시키는 지를 고려해서 Reflow 혹은 Repaint를 발생시키는 속성은 최대한 지양해야 합니다.

매 프레임마다 실행되는 반복 애니메이션의 경우에는 더더욱 이러한 부분들이 중요합니다. 돈이 떨어지는 애니메이션은 프로젝트에서 계속 반복하게 해놔서 매번 실행하게 되는 데 불필요한 Repaint 단계를 줄였을 뿐인데 20%이상의 성능 향상 을 확인할 수 있었습니다.

추가적으로 브라우저의 렌더링 과정, Pixel Pipeline에 대해서도 더 공부를 해야겠다고 생각했습니다. 500개로 늘려서 측정을 했을 때 Paint 과정은 획기적으로 줄었는 데 왜 Rendering 과정에 걸리는 시간은 늘었는 지 모르겠습니다. Paint가 없어지고 Composite 단계에서 연산이 늘어나면서 Rendering 시간이 증가한 것이라는 데, 이 부분에 대해서는 각 렌더링 과정이 어떻게 이뤄지는 지 등 브라우저 렌더링에 대해서 더 자세하게 공부를 해봐야 겠습니다. 더불어 LightHouse의 각 항목에 대해서도 알아봐야 겠습니다.

Google Developers의 자료를 보면 여러 성능 향상 기법들이 소개되어 있는 데 이 자료를 참고해서 프로젝트를 진행할 때 성능에 대해서 신경쓰면서 진행하면 좋을 것 같습니다. 성능 향상 기법 외에 병목 현상 원인을 찾는 방법에 대해서도 나와있으니 이러한 부분들도 다음에 문제를 찾을 때 참고해야 겠네요. 이 자료도 멘토님께서 소개해주셨습니다 ㅎㅎ


2021.08.26 추가

알고 보니까 수정할 때 transform이 일어나는 지점을 70%에서 100%로 바꾸면서 transform에 대한 애니메이션이 수정 전보다 더 일어나고 있었습니다. 이로 인해 Rendering 과정에 걸리는 시간이 더 늘어났을 가능성이 있을 것 같네요.

profile
꾸준함을 만들고 싶어요

6개의 댓글

comment-user-thumbnail
2021년 8월 15일

윤서님 아티클 정말 잘 봤습니다!! 성능 측정은 잘 안하게 됐는데 저도 이번에 한 번 살펴봐야겠어요 ㅎㅎ

같은 css 속성이라도 브라우저 엔진에 따라 layout, paint, composite를 일으키는게 다르다고 합니다
아래 사이트를 추천드려요! 속성/엔진별로 구분되어 있습니다..

https://csstriggers.com/

1개의 답글
comment-user-thumbnail
2021년 8월 16일

잘 읽었습니다. 수정 과정에서 border-radius 속성을 제거한 것 같은데, 첨부하신 막대 그래프를 보면 오히려 평균 렌더링 시간은 증가한 것 같아요. 이유가 뭘까요?

1개의 답글
comment-user-thumbnail
2021년 8월 17일

잘 읽었습니다 : )

1개의 답글