렌더링 성능 향상을 위한 버벅거림 무효화

Tom Wiltzius
Tom Wiltzius

소개

애니메이션, 전환 및 기타 작은 UI 효과를 실행할 때 웹 앱이 반응하고 매끄럽게 느껴져야 합니다. 이러한 효과에 버벅거림이 없는지 확인하는 것은 '네이티브'한 느낌과 투박하고 세련되지 않은 효과를 구분할 수 있습니다.

이 글은 브라우저의 렌더링 성능 최적화에 관한 문서 시리즈 중 첫 번째입니다. 먼저 부드러운 애니메이션이 어려운 이유와 이를 위해 필요한 작업, 그리고 몇 가지 간단한 권장사항을 살펴보겠습니다. 이러한 아이디어 중 다수는 올해 Google I/O 강연 (동영상)에서 Nat Duca와 제가 진행한 강연인 'Jank Busters'에서 소개되었습니다.

V-sync 소개

PC 게이머에게는 이 용어가 익숙할 수 있지만 웹에서는 흔히 사용되지 않습니다. v-sync란 무엇인가요?

휴대전화의 디스플레이는 일정한 간격으로 새로고침됩니다 (항상 그렇지는 않지만 1초에 약 60회). V-sync (또는 수직 동기화)는 화면 새로고침 사이에만 새 프레임을 생성하는 방법을 의미합니다. 이를 화면 버퍼에 데이터를 쓰는 프로세스와 해당 데이터를 읽는 운영체제가 디스플레이에 데이터를 표시하는 프로세스 사이의 경합 상태로 생각할 수 있습니다. 버퍼링된 프레임 콘텐츠가 새로고침되는 동안이 아니라 새로고침 간에 변경되도록 해야 합니다. 그렇지 않으면 모니터에 한 프레임의 절반과 다른 프레임의 절반이 표시되어 '테어링'이 발생합니다.

애니메이션을 원활하게 만들려면 화면을 새로고침할 때마다 새 프레임을 준비해야 합니다. 이는 프레임 시간 (즉, 프레임을 준비해야 하는 시점)과 프레임 예산 (즉, 브라우저에서 프레임을 생성하는 시간)이라는 두 가지 중요한 영향을 줍니다. 프레임을 완료할 때까지 화면 새로고침 사이에만 시간 (60Hz 화면에서 최대 16ms)이 있고 마지막 프레임이 화면에 표시되는 즉시 다음 프레임을 생성하려고 합니다.

타이밍의 중요성: requestAnimationFrame

많은 웹 개발자는 16밀리초마다 setInterval 또는 setTimeout를 사용하여 애니메이션을 만듭니다. 여기에는 여러 가지 이유가 있지만 (잠시 후에 자세히 설명하지만) 특히 우려되는 사항은 다음과 같습니다.

  • JavaScript의 타이머 분해능은 수 밀리초 단위입니다.
  • 기기마다 화면 재생 빈도가 다릅니다.

위에서 언급한 프레임 타이밍 문제를 떠올려 보세요. 다음 화면 새로고침이 이루어지기 전에 자바스크립트, DOM 조작, 레이아웃, 페인팅 등으로 완성된 애니메이션 프레임이 필요합니다. 타이머 해상도가 낮으면 다음 화면을 새로 고치기 전에 애니메이션 프레임을 완료하기가 어려울 수 있지만, 고정 타이머를 사용하는 경우 화면 새로고침 빈도의 차이는 불가능할 수 있습니다. 타이머 간격에 관계없이 프레임의 타이밍 창을 천천히 넘다가 결국 하나가 삭제됩니다. 이는 타이머가 밀리초 단위의 정확도로 실행된 경우에도 발생하며 이는 개발자가 발견한 것처럼 작동하지 않습니다. 타이머 해상도는 머신이 배터리를 사용 중인지 전원에 연결되어 있는지에 따라 달라지며, 리소스를 지나치게 많이 소비하는 백그라운드 탭의 영향을 받을 수 있습니다. 드물게 발생하는 경우(예: 1밀리초마다 꺼졌기 때문에 16프레임마다) 1초의 프레임이 누락되는 것을 확인할 수 있습니다. 또한 표시되지 않는 프레임을 생성하는 작업도 수행하게 됩니다. 이 경우 애플리케이션에서 다른 작업을 할 때 소비할 수 있는 전력과 CPU 시간이 낭비됩니다.

디스플레이마다 화면 재생 빈도가 다릅니다. 60Hz가 일반적이지만 일부 휴대전화는 59Hz이며 일부 노트북은 저전력 모드에서 50Hz로 감소하며 일부 데스크톱 모니터는 70Hz입니다.

우리는 렌더링 성능을 논의할 때 FPS (초당 프레임 수)에 중점을 두는 경향이 있지만 편차는 더 큰 문제가 될 수 있습니다. 우리 눈은 타이밍이 좋지 않은 애니메이션으로 일어날 수 있는 애니메이션의 작고 불규칙한 타격을 알아차립니다.

올바르게 시간이 지정된 애니메이션 프레임을 가져오는 방법은 requestAnimationFrame를 사용하는 것입니다. 이 API를 사용하면 브라우저에 애니메이션 프레임을 요청합니다. 브라우저가 곧 새 프레임을 생성할 때 콜백이 호출됩니다. 새로고침 빈도와 관계없이 발생합니다.

requestAnimationFrame에는 다른 좋은 속성도 있습니다.

  • 백그라운드 탭의 애니메이션이 일시중지되어 시스템 리소스와 배터리 수명이 절약됩니다.
  • 시스템이 화면의 화면 재생 빈도로 렌더링을 처리할 수 없으면 애니메이션을 제한하고 콜백을 덜 자주 생성할 수 있습니다 (예: 60Hz 화면에서 초당 30회). 이렇게 하면 프레임 속도가 절반으로 떨어지지만 애니메이션이 일관되게 유지됩니다. 위에서 언급한 것처럼 사용자의 눈은 프레임 속도보다 변동에 훨씬 더 민감합니다. 안정적인 30Hz가 초당 몇 프레임을 놓친 60Hz보다 더 좋아 보입니다.

requestAnimationFrame에 대해서는 이미 전체적으로 살펴보았으므로 자세한 내용은 creative JS와 같은 도움말을 참고하세요. 하지만 애니메이션을 매끄럽게 하기 위한 중요한 첫 걸음이 될 것입니다.

프레임 예산

화면을 새로 고칠 때마다 새 프레임을 준비해야 하므로 새 프레임을 만들기 위한 모든 작업을 할 수 있는 새로고침 사이에 오는 시간만 있습니다. 60Hz 디스플레이에서는 모든 JavaScript를 실행하고, 레이아웃과 페인트를 수행하고, 프레임을 꺼내기 위해 브라우저가 해야 하는 다른 모든 작업을 수행하는 데 약 16ms가 걸린다는 것을 의미합니다. 즉, requestAnimationFrame 콜백 내의 JavaScript를 실행하는 데 16ms보다 오래 걸리는 경우 v-sync에 관한 시간 내에 프레임을 생성할 방법이 없습니다.

16ms는 긴 시간이 아닙니다. 다행히 Chrome의 개발자 도구를 사용하면 requestAnimationFrame 콜백 중에 프레임 예산을 소진하는 경우 추적할 수 있습니다.

Dev Tools 타임라인을 열고 이 애니메이션의 실제 동작을 녹화하면 애니메이션 작업 시 예산을 초과했음을 알 수 있습니다. 타임라인에서 'Frames'로 전환하여 다음을 살펴봅니다.

레이아웃이 지나치게 많은 데모
레이아웃이 너무 많은 데모

이러한 requestAnimationFrame (rAF) 콜백은 200밀리초 이상 걸립니다. 16ms마다 프레임을 처리하기에는 엄청나게 깁니다. 이러한 긴 rAF 콜백 중 하나를 열면 내부에서 어떤 일이 일어나고 있는지를 알 수 있습니다. 이 경우에는 많은 레이아웃이 있습니다.

폴의 동영상에서는 재레이아웃의 구체적인 원인 (scrollTop를 읽음)과 이를 방지하는 방법을 자세히 설명합니다. 하지만 여기서 요점은 콜백에 대해 자세히 알아보고 무엇이 너무 오래 걸리는지 조사할 수 있다는 것입니다.

레이아웃이 대폭 줄어든 업데이트된 데모
레이아웃이 크게 줄어든 업데이트된 데모

프레임 시간은 16ms입니다. 프레임의 이 빈 공간은 더 많은 작업을 해야 하는 헤드룸입니다 (또는 브라우저가 백그라운드에서 해야 하는 작업을 하도록 하세요). 그 빈 공간이 좋죠.

버벅거림의 기타 원인

JavaScript 기반 애니메이션을 실행하려고 할 때 문제가 발생하는 가장 큰 이유는 다른 요소가 rAF 콜백을 방해하고 심지어는 전혀 실행되지 않을 수 있기 때문입니다. rAF 콜백이 가볍고 단 몇 밀리초만에 실행되더라도 다른 활동 (예: 방금 들어온 XHR 처리, 입력 이벤트 핸들러 실행, 타이머에서 예약된 업데이트 실행)은 갑자기 들어오고 실행 시 아무런 조치를 취하지 않고 실행될 수 있습니다. 휴대기기에서는 이러한 이벤트를 처리하는 데 수백 밀리초가 걸릴 수 있으며 그동안 애니메이션이 완전히 중단됩니다. 이러한 애니메이션 문제를 버벅거림이라고 합니다.

이러한 상황을 피할 수 있는 완벽한 해결책은 없지만 성공을 위해 준비할 수 있는 몇 가지 아키텍처 관련 권장사항은 있습니다.

  • 입력 핸들러에서 너무 많은 처리를 수행하지 마세요. 예를 들어 onscroll 핸들러에서 많은 양의 JS를 수행하거나 전체 페이지를 다시 정렬하려고 시도하는 것은 끔찍한 버벅거림의 매우 일반적인 원인입니다.
  • 가능한 한 많은 처리 (읽기: 실행하는 데 시간이 오래 걸리는 모든 항목)를 rAF 콜백 또는 웹 워커에 푸시합니다.
  • 작업을 rAF 콜백으로 푸시하는 경우 프레임별로 조금씩만 처리하거나 중요한 애니메이션이 끝날 때까지 지연되도록 하세요. 이렇게 하면 계속해서 짧은 rAF 콜백을 실행하고 부드럽게 애니메이션을 적용할 수 있습니다.

입력 핸들러가 아닌 requestAnimationFrame 콜백으로 처리를 푸시하는 방법에 관한 자세한 내용은 Paul Lewis의 requestAnimationFrame을 사용한 더 편안하고 평균적이며 빠른 애니메이션 문서를 참고하세요.

CSS 애니메이션

이벤트 및 rAF 콜백에서 경량 JS보다 더 나은 것은 무엇인가요? 자바스크립트는 없습니다.

앞서 rAF 콜백 중단을 방지하는 완벽한 방법은 없지만 CSS 애니메이션을 사용하면 콜백이 전혀 필요하지 않도록 할 수 있습니다. 특히 Android용 Chrome (및 유사한 기능을 지원하는 다른 브라우저)에서 CSS 애니메이션에는 JavaScript가 실행 중인 경우에도 브라우저가 종종 실행할 수 있는 매우 바람직한 속성이 있습니다.

위 섹션에는 버벅거림에 관한 암시적 명령문이 있습니다. 브라우저는 한 번에 한 가지 작업만 할 수 있습니다. 엄밀히 말하면 아닐 수도 있지만, 브라우저가 JS 실행, 레이아웃 또는 페인팅을 수행할 수 있으며 한 번에 하나씩만 실행할 수 있다는 가정이 좋습니다. 이는 개발자 도구의 타임라인 뷰에서 확인할 수 있습니다. 이 규칙의 예외 중 하나는 Android용 Chrome (데스크톱용 Chrome에서도 곧 출시 예정)의 CSS 애니메이션입니다.

가능한 경우 CSS 애니메이션을 사용하면 애플리케이션이 간소화되고 JavaScript가 실행되는 동안에도 애니메이션이 원활하게 실행될 수 있습니다.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

버튼을 클릭하면 JavaScript가 180ms 동안 실행되어 버벅거림이 발생합니다. 하지만 CSS 애니메이션으로 애니메이션을 구동하면 버벅거림이 더 이상 발생하지 않습니다.

(이 글을 작성하는 시점에서 CSS 애니메이션은 데스크톱 Chrome이 아닌 Android용 Chrome에서만 버벅거림이 없습니다.)

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

CSS 애니메이션 사용에 대한 자세한 내용은 MDN에 관한 이 도움말을 참고하세요.

요약

간단히 말씀드리면 다음과 같습니다.

  1. 애니메이션을 적용할 때 모든 화면 새로고침에 대한 프레임을 생성하는 것이 중요합니다. Vsync 애니메이션은 앱의 느낌에 매우 긍정적인 영향을 줍니다.
  2. Chrome 및 기타 최신 브라우저에서 vsync'd 애니메이션을 가져오는 가장 좋은 방법은 CSS 애니메이션을 사용하는 것입니다. CSS 애니메이션에서 제공하는 것보다 더 많은 유연성이 필요한 경우 requestAnimationFrame 기반 애니메이션을 사용하는 것이 가장 좋습니다.
  3. rAF 애니메이션을 양호한 상태로 원활하게 유지하려면 다른 이벤트 핸들러가 rAF 콜백 실행을 방해하지 않도록 하고 rAF 콜백을 짧게(15ms 미만) 유지합니다.

마지막으로 vsync'd 애니메이션은 단순한 UI 애니메이션에만 적용되는 것이 아니라 Canvas2D 애니메이션, WebGL 애니메이션 및 정적 페이지의 스크롤에도 적용됩니다. 이 시리즈의 다음 도움말에서는 이러한 개념을 염두에 두고 스크롤 성능을 자세히 알아봅니다.

감사합니다.

참조