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

Tom Wiltzius
Tom Wiltzius

소개

애니메이션, 전환 및 기타 작은 UI 효과를 실행할 때 웹 앱이 반응하고 원활하게 작동해야 합니다. 이러한 효과에 버벅거림이 발생하지 않으면 '네이티브' 효과와 투박하고 꾸미지 않은 느낌을 줍니다.

이 도움말은 브라우저에서 렌더링 성능 최적화를 다루는 도움말 시리즈 중 첫 번째입니다. 먼저 매끄러운 애니메이션이 어려운 이유와 이를 달성하기 위해 필요한 작업, 그리고 몇 가지 간단한 모범 사례를 살펴보겠습니다. 이러한 아이디어의 대부분은 원래 'Jank Busters'에 있었는데 올해 Google I/O 강연 (동영상)에서 Nat Duca님과 제가 진행한 강연입니다.

V-sync 소개

PC 게이머들은 잘 알고 있을 수 있지만 웹에서는 드문 용어입니다. v-sync란 무엇인가요?

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

매끄러운 애니메이션을 얻으려면 화면 새로고침이 발생할 때마다 새 프레임이 준비되어야 합니다. 여기에는 프레임 시간 (프레임이 준비되어야 하는 시점)과 프레임 예산 (브라우저가 프레임을 생성해야 하는 시간)이라는 두 가지 큰 의미가 있습니다. 프레임을 완료하기 위한 화면 새로고침 사이의 시간만 있고 (60Hz 화면에서 최대 16ms) 마지막 프레임이 화면에 표시되자마자 다음 프레임 생성을 시작하려고 합니다.

타이밍의 중요성: requestAnimationFrame

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

  • JavaScript의 타이머 확인은 몇 밀리초 정도만 됩니다.
  • 기기마다 화면 재생 빈도가 다릅니다.

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

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

렌더링 성능을 논의할 때는 초당 프레임 수 (FPS)에 초점을 맞추는 경향이 있지만 편차가 더 큰 문제가 될 수 있습니다. 우리의 눈은 애니메이션에서 타이밍이 좋지 않은 애니메이션에서 만들 수 있는 작고 불규칙한 장애물을 발견합니다.

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

requestAnimationFrame에는 다른 멋진 속성도 있습니다.

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

requestAnimationFrame는 이미 도처에서 논의되었으므로 광고 소재 JS의 도움말과 같은 도움말에서 자세한 내용을 확인하세요. 이는 애니메이션을 원활하게 만드는 중요한 첫 번째 단계입니다.

프레임 예산

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

16ms는 많은 시간이 아닙니다. 다행히 Chrome의 개발자 도구를 사용하면 requestAnimationFrame 콜백 중에 프레임 예산이 초과되는지 추적할 수 있습니다.

Dev Tools 타임라인을 열고 이 애니메이션을 실제로 녹화하면 애니메이션 제작 시 예산을 많이 초과했음을 쉽게 알 수 있습니다. 타임라인에서 '프레임'으로 전환 을 살펴보세요.

<ph type="x-smartling-placeholder">
</ph> 레이아웃이 지나치게 많은 데모
레이아웃이 지나치게 많은 데모

이러한 requestAnimationFrame (rAF) 콜백이 200ms 이상 걸립니다. 16ms마다 프레임을 실행하기에는 엄청나게 긴 셈입니다. 이러한 긴 rAF 콜백 중 하나를 열면 내부에 있는 내용, 즉 많은 레이아웃이 표시됩니다.

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

<ph type="x-smartling-placeholder">
</ph> 레이아웃이 크게 축소된 업데이트된 데모
레이아웃이 크게 축소된 업데이트된 데모

프레임 시간이 16ms임을 알 수 있습니다. 프레임의 빈 공간이 더 많은 작업을 해야 하거나 브라우저가 백그라운드에서 실행해야 하는 작업을 수행할 여유 공간이 됩니다. 그 빈 공간은 다행이지.

버벅거림의 기타 원인

JavaScript 기반 애니메이션을 실행하려고 할 때 문제가 발생하는 가장 큰 원인 다른 것들이 rAF 콜백을 방해할 수 있고 심지어 전혀 실행되지 않습니다 rAF 콜백이 가볍고 단 몇 초 만에 실행되더라도 밀리초, 다른 활동 (예: 방금 들어온 XHR 처리, 입력 이벤트 핸들러를 실행하거나 타이머에서 예약된 업데이트를 실행하는 등)는 양보 없이 어떤 기간 동안이나 실행되는 것을 볼 수 있습니다. 휴대기기의 경우 이러한 이벤트를 처리하는 장치는 수백 밀리초가 걸릴 수도 있지만, 이 시간 동안 애니메이션이 완전히 중단됩니다. 이를 애니메이션에 버벅거림이 발생합니다.

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

  • 입력 핸들러에서 많은 작업을 실행하지 마세요. 많은 JavaScript를 수행하거나 전체 페이지를 다시 정렬하려는 경우(예: onscroll 핸들러는 끔찍한 버벅거림의 매우 일반적인 원인입니다.
  • rAF 콜백 또는 Web Workers에 최대한 많은 처리 (읽기, 실행에 시간이 오래 걸리는 항목)를 푸시합니다.
  • 작업을 rAF 콜백으로 푸시하는 경우 프레임마다 조금씩만 처리하거나 중요한 애니메이션이 끝날 때까지 지연하도록 분할해 보세요. 그러면 짧은 rAF 콜백을 계속 실행하고 매끄럽게 애니메이션을 적용할 수 있습니다.

입력 핸들러가 아닌 requestAnimationFrame 콜백으로 처리를 푸시하는 방법에 관한 자세한 튜토리얼은 폴 루이스의 자료 requestAnimationFrame으로 더 간결하고, 평균, 빠른 애니메이션을 참고하세요.

CSS 애니메이션

이벤트 및 rAF 콜백에서 경량형 JS보다 나은 점은 무엇인가요? JavaScript가 없습니다.

앞서 rAF 콜백이 중단되는 것을 피할 수 있는 만병통치약은 없다고 말했지만, CSS 애니메이션을 사용하면 rAF 콜백이 완전히 필요하지 않도록 할 수 있습니다. 특히 Android용 Chrome (그리고 다른 브라우저에서 유사한 기능을 개발 중)에서 CSS 애니메이션은 JavaScript가 실행 중이더라도 브라우저가 이를 실행할 수 있는 매우 바람직한 속성이 있습니다.

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

가능한 경우 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된 애니메이션을 가져오는 가장 좋은 방법은 CSS 애니메이션을 사용할 수 있습니다. CSS 애니메이션보다 유연성이 더 필요한 경우 가장 좋은 방법은 requestAnimationFrame 기반 애니메이션입니다.
  3. rAF 애니메이션을 건강하고 원활하게 유지하려면 다른 이벤트 핸들러가 rAF 콜백이 실행되는 것을 방해하지 않고 rAF 콜백을 유지해야 합니다. 짧게(15밀리초 미만)

마지막으로 vsync의 애니메이션은 간단한 UI 애니메이션에만 적용되지 않습니다. Canvas2D 애니메이션, WebGL 애니메이션, 정적 페이지의 스크롤에도 적용됩니다. 이 시리즈의 다음 도움말에서는 이러한 개념을 염두에 두고 스크롤 성능을 자세히 살펴보겠습니다.

즐거운 애니메이션 되세요.

참조