소개
애니메이션, 전환, 기타 작은 UI 효과를 사용할 때 웹 앱이 반응성이 좋고 부드럽게 느껴지기를 원합니다. 이러한 효과가 버벅거림 없이 작동하는지 확인하는 것은 '네이티브' 느낌과 투박하고 세련되지 않은 느낌의 차이를 의미할 수 있습니다.
이 도움말은 브라우저의 렌더링 성능 최적화를 다루는 일련의 도움말 중 첫 번째입니다. 먼저 부드러운 애니메이션이 어려운 이유와 이를 달성하기 위해 필요한 사항, 몇 가지 간단한 권장사항을 살펴보겠습니다. 이러한 아이디어 중 상당수는 올해 Google I/O에서 Nat Duca와 제가 진행한 'Jank Busters'라는 강연 (동영상)에서 처음 소개되었습니다.
V-sync 소개
PC 게이머에게는 익숙한 용어일 수 있지만 웹에서는 흔하지 않은 수직 동기화란 무엇인가요?
휴대전화의 디스플레이를 생각해 보세요. 디스플레이는 일정한 간격으로 새로고침되며, 일반적으로 (항상은 아님) 초당 약 60회 새로고침됩니다. 수직 동기화 (V-sync)는 화면 새로고침 사이에만 새 프레임을 생성하는 방식을 말합니다. 이는 화면 버퍼에 데이터를 쓰는 프로세스와 해당 데이터를 읽어 디스플레이에 표시하는 운영체제 간의 경합 상태와 비슷하다고 생각할 수 있습니다. 버퍼링된 프레임 콘텐츠는 새로고침 중에 변경되는 것이 아니라 새로고침 사이에 변경되어야 합니다. 그렇지 않으면 모니터에 한 프레임의 절반과 다른 프레임의 절반이 표시되어 '티어링'이 발생합니다.
부드러운 애니메이션을 얻으려면 화면 새로고침이 발생할 때마다 새 프레임을 준비해야 합니다. 이는 프레임 시간 (즉, 프레임이 준비되어야 하는 시점)과 프레임 예산 (즉, 브라우저가 프레임을 생성해야 하는 시간)이라는 두 가지 큰 의미를 갖습니다. 화면 새로고침 사이에 프레임을 완료할 시간 (60Hz 화면에서 약 16ms)만 있으며, 마지막 프레임이 화면에 표시되는 즉시 다음 프레임을 생성해야 합니다.
타이밍이 중요합니다: requestAnimationFrame
많은 웹 개발자가 16밀리초마다 setInterval 또는 setTimeout를 사용하여 애니메이션을 만듭니다. 이는 여러 가지 이유로 문제가 되며 (잠시 후에 자세히 설명하겠습니다), 특히 다음과 같은 문제가 있습니다.
- JavaScript의 타이머 해상도는 몇 밀리초 단위에 불과합니다.
- 기기마다 화면 재생 빈도가 다름
위에서 언급한 프레임 타이밍 문제를 다시 떠올려 보세요. 다음 화면 새로고침이 발생하기 전에 JavaScript, DOM 조작, 레이아웃, 페인팅 등으로 완료된 애니메이션 프레임이 준비되어 있어야 합니다. 타이머 해상도가 낮으면 다음 화면 새로고침 전에 애니메이션 프레임을 완료하기 어려울 수 있지만, 화면 새로고침 빈도가 다양하면 고정 타이머로는 불가능합니다. 타이머 간격이 얼마이든 프레임의 타이밍 기간에서 서서히 벗어나 결국 프레임을 삭제하게 됩니다. 이는 타이머가 밀리초 단위의 정확도로 실행되더라도 발생할 수 있습니다. 하지만 개발자가 발견한 바와 같이 타이머는 밀리초 단위의 정확도로 실행되지 않습니다. 타이머 해상도는 기기가 배터리로 작동하는지 아니면 전원에 연결되어 있는지에 따라 달라지며, 리소스를 독점하는 백그라운드 탭의 영향을 받을 수 있습니다. 이러한 현상이 드물게 발생하더라도(예: 1밀리초 차이로 인해 16프레임마다 발생) 초당 여러 프레임이 누락되는 것을 확인할 수 있습니다. 또한 표시되지 않는 프레임을 생성하는 작업을 수행하게 되므로 전력과 CPU 시간이 낭비되어 애플리케이션에서 다른 작업을 수행하는 데 사용할 수 있습니다.
디스플레이마다 화면 재생 빈도가 다릅니다. 60Hz가 일반적이지만 일부 휴대전화는 59Hz이고, 일부 노트북은 저전력 모드에서 50Hz로 떨어지며, 일부 데스크톱 모니터는 70Hz입니다.
렌더링 성능을 논의할 때 초당 프레임 수 (FPS)에 집중하는 경향이 있지만, 편차가 더 큰 문제가 될 수 있습니다. 우리의 눈은 타이밍이 잘못된 애니메이션이 생성할 수 있는 작고 불규칙한 애니메이션 결함을 알아차립니다.
정확한 타이밍의 애니메이션 프레임을 가져오는 방법은 requestAnimationFrame를 사용하는 것입니다. 이 API를 사용하면 브라우저에 애니메이션 프레임을 요청하게 됩니다. 브라우저에서 곧 새 프레임을 생성할 때 콜백이 호출됩니다. 이는 화면 재생 빈도와 관계없이 발생합니다.
requestAnimationFrame에는 다음과 같은 다른 유용한 속성도 있습니다.
- 백그라운드 탭의 애니메이션이 일시중지되어 시스템 리소스와 배터리 수명이 절약됩니다.
- 시스템이 화면 재생 빈도로 렌더링을 처리할 수 없는 경우 애니메이션을 제한하고 콜백을 덜 자주 생성할 수 있습니다 (예: 60Hz 화면에서 초당 30회). 이렇게 하면 프레임 속도가 절반으로 떨어지지만 애니메이션은 일관되게 유지됩니다. 위에서 언급했듯이 사람의 눈은 프레임 속도보다 변화에 훨씬 더 민감합니다. 초당 몇 프레임을 놓치는 60Hz보다 안정적인 30Hz가 더 좋아 보입니다.
requestAnimationFrame는 이미 여러 곳에서 논의되었으므로 Creative JS의 이 도움말과 같은 도움말을 참고하여 자세한 내용을 확인하세요. 하지만 이는 부드러운 애니메이션을 위한 중요한 첫 단계입니다.
프레임 예산
화면이 새로고침될 때마다 새 프레임이 준비되어야 하므로 새로고침 사이에만 새 프레임을 만드는 모든 작업을 할 수 있습니다. 60Hz 디스플레이에서는 모든 JavaScript를 실행하고, 레이아웃을 수행하고, 페인트하고, 브라우저가 프레임을 가져오기 위해 해야 하는 다른 모든 작업을 수행하는 데 약 16ms가 걸립니다. 즉, requestAnimationFrame 콜백 내의 JavaScript를 실행하는 데 16ms 이상 걸리면 v-sync에 맞춰 프레임을 생성할 수 없습니다.
16ms는 긴 시간이 아닙니다. 다행히 Chrome의 개발자 도구를 사용하면 requestAnimationFrame 콜백 중에 프레임 예산을 초과하는지 추적할 수 있습니다.
DevTools 타임라인을 열고 이 애니메이션의 작동을 기록하면 애니메이션을 적용할 때 예산을 훨씬 초과한다는 것을 빠르게 알 수 있습니다. 타임라인에서 'Frames'로 전환하여 살펴보세요.
requestAnimationFrame (rAF) 콜백이 200ms를 초과합니다. 이는 16ms마다 프레임을 표시하기에는 너무 긴 시간입니다. 긴 rAF 콜백 중 하나를 열면 내부에서 무슨 일이 일어나고 있는지 알 수 있습니다. 이 경우에는 레이아웃이 많습니다.
폴의 동영상에서는 레이아웃 재구성의 구체적인 원인(scrollTop 읽기)과 이를 방지하는 방법을 자세히 설명합니다. 여기서 중요한 점은 콜백을 자세히 살펴보고 시간이 오래 걸리는 원인을 조사할 수 있다는 것입니다.
16ms 프레임 시간을 확인합니다. 프레임의 빈 공간은 더 많은 작업을 수행할 수 있는 여유 공간입니다 (또는 브라우저가 백그라운드에서 필요한 작업을 수행하도록 허용). 이러한 공백은 좋은 것입니다.
버벅거림의 기타 원인
JavaScript 기반 애니메이션을 실행하려고 할 때 발생하는 가장 큰 문제는 다른 요소가 rAF 콜백을 방해하고 실행을 완전히 막을 수도 있다는 것입니다. rAF 콜백이 간결하고 몇 밀리초 만에 실행되더라도 다른 활동 (예: 방금 들어온 XHR 처리, 입력 이벤트 핸들러 실행, 타이머에서 예약된 업데이트 실행)이 갑자기 들어와 양보 없이 일정 기간 실행될 수 있습니다. 휴대기기에서 이러한 이벤트를 처리하는 데 수백 밀리초가 걸릴 수 있으며, 이 시간 동안 애니메이션이 완전히 중단됩니다. 이러한 애니메이션 끊김 현상을 버벅거림이라고 합니다.
이러한 상황을 피할 수 있는 만병통치약은 없지만 성공을 위한 몇 가지 아키텍처 권장사항이 있습니다.
- 입력 핸들러에서 많은 처리를 수행하지 마세요. JS를 많이 사용하거나 스크롤 핸들러 중에 전체 페이지를 재정렬하려고 하면 심각한 버벅거림이 발생하는 경우가 많습니다.
- 가능한 한 많은 처리 (실행하는 데 시간이 오래 걸리는 모든 작업)를 rAF 콜백 또는 웹 작업자로 푸시합니다.
- rAF 콜백에 작업을 푸시하는 경우, 각 프레임에서 조금씩만 처리하거나 중요한 애니메이션이 끝날 때까지 지연하도록 작업을 청크로 분할해 보세요. 이렇게 하면 짧은 rAF 콜백을 계속 실행하고 원활하게 애니메이션을 처리할 수 있습니다.
입력 핸들러가 아닌 requestAnimationFrame 콜백으로 처리를 푸시하는 방법을 다루는 유용한 가이드는 Paul Lewis의 도움말 requestAnimationFrame을 사용한 더 가볍고, 더 강력하고, 더 빠른 애니메이션을 참고하세요.
CSS 애니메이션
이벤트 및 rAF 콜백에서 가벼운 JS보다 더 좋은 것은 무엇일까요? JS 없음.
앞서 rAF 콜백 중단을 방지하는 만병통치약은 없다고 말씀드렸지만, CSS 애니메이션을 사용하면 rAF 콜백의 필요성을 완전히 없앨 수 있습니다. 특히 Android용 Chrome (다른 브라우저에서도 유사한 기능을 개발 중)에서 CSS 애니메이션은 JavaScript가 실행 중인 경우에도 브라우저에서 실행할 수 있는 매우 바람직한 속성을 가지고 있습니다.
버벅거림에 관한 위의 섹션에는 암시적인 문구가 있습니다. 브라우저는 한 번에 한 가지만 할 수 있습니다. 이것은 엄밀히 말하면 사실이 아니지만, 브라우저가 언제든지 JS를 실행하거나 레이아웃을 수행하거나 페인팅을 수행할 수 있지만 한 번에 하나만 수행할 수 있다는 가정을 세우는 것이 좋습니다. 이는 Dev Tools의 타임라인 뷰에서 확인할 수 있습니다. 이 규칙의 예외 중 하나는 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의 이 도움말을 참고하세요.
마무리
요약하자면 다음과 같습니다.
- 애니메이션을 적용할 때는 모든 화면 새로고침에 프레임을 생성하는 것이 중요합니다. Vsync 애니메이션은 앱의 느낌에 매우 긍정적인 영향을 미칩니다.
- Chrome 및 기타 최신 브라우저에서 VSYNC 애니메이션을 가져오는 가장 좋은 방법은 CSS 애니메이션을 사용하는 것입니다. CSS 애니메이션이 제공하는 것보다 더 많은 유연성이 필요한 경우 가장 좋은 기법은 requestAnimationFrame 기반 애니메이션입니다.
- rAF 애니메이션을 정상적으로 유지하려면 다른 이벤트 핸들러가 rAF 콜백 실행을 방해하지 않도록 하고 rAF 콜백을 짧게 (<15ms) 유지하세요.
마지막으로, vsync 애니메이션은 간단한 UI 애니메이션에만 적용되는 것이 아니라 Canvas2D 애니메이션, WebGL 애니메이션, 정적 페이지 스크롤에도 적용됩니다. 이 시리즈의 다음 도움말에서는 이러한 개념을 염두에 두고 스크롤 성능을 자세히 살펴보겠습니다.
즐겁게 애니메이션을 만들어 보세요.