소개
애니메이션, 전환, 기타 작은 UI 효과를 실행할 때 웹 앱이 반응이 빠르고 부드럽게 느껴지도록 해야 합니다. 이러한 효과가 버벅거림이 없도록 하면 '네이티브' 느낌을 주거나 투박하고 미완성된 느낌을 줄 수 있습니다.
이 도움말은 브라우저의 렌더링 성능 최적화를 다루는 일련의 도움말 중 첫 번째입니다. 먼저 부드러운 애니메이션을 구현하기가 어려운 이유와 이를 달성하기 위해 필요한 사항, 간단한 권장사항을 살펴봅니다. 이러한 아이디어의 대부분은 올해 Google I/O 강연 (동영상)에서 냇 두카와 제가 발표한 'Jank Busters'에서 처음 소개되었습니다.
V-sync 소개
PC 게이머는 이 용어에 익숙할 수 있지만 웹에서는 흔하지 않습니다. v-sync란 무엇인가요?
휴대전화의 디스플레이를 생각해 보세요. 디스플레이는 일정한 간격으로 새로고침되며, 일반적으로 초당 약 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 도움말과 같은 도움말에서 자세한 내용을 확인하세요. requestAnimationFrame
는 부드러운 애니메이션을 위한 중요한 첫 번째 단계입니다.
프레임 예산
화면을 새로고침할 때마다 새 프레임을 준비해야 하므로 새로고침 사이에 새 프레임을 만드는 모든 작업을 실행할 수 있는 시간만 있습니다. 60Hz 디스플레이에서는 모든 JavaScript를 실행하고, 레이아웃을 실행하고, 페인트하고, 브라우저가 프레임을 표시하기 위해 해야 하는 다른 작업을 실행하는 데 약 16ms가 소요됩니다. 즉, requestAnimationFrame
콜백 내 JavaScript가 실행되는 데 16밀리초가 넘게 걸리면 v-sync에 맞춰 프레임을 생성할 수 없습니다.
16ms는 그리 긴 시간이 아닙니다. 다행히 Chrome의 개발자 도구를 사용하면 requestAnimationFrame 콜백 중에 프레임 예산이 초과되는지 추적할 수 있습니다.
Dev Tools 타임라인을 열고 이 애니메이션이 실행되는 모습을 녹화해 보면 애니메이션을 실행할 때 예산을 훨씬 초과하는 것을 확인할 수 있습니다. 타임라인에서 '프레임'으로 전환하여 살펴봅니다.
이러한 requestAnimationFrame (rAF) 콜백이 200ms 이상 소요됩니다. 16ms마다 프레임을 표시하기에는 너무 긴 시간입니다. 긴 rAF 콜백 중 하나를 열면 내부에서 어떤 일이 일어나고 있는지 확인할 수 있습니다. 이 경우 많은 레이아웃이 있습니다.
폴의 동영상에서는 레이아웃 변경의 구체적인 원인 (scrollTop
읽기)과 이를 방지하는 방법을 자세히 설명합니다. 중요한 점은 콜백을 자세히 살펴보고 시간이 오래 걸리는 원인을 조사할 수 있다는 것입니다.
프레임 시간이 16ms임을 확인합니다. 프레임의 빈 공간은 더 많은 작업을 하거나 브라우저가 백그라운드에서 해야 하는 작업을 실행할 수 있는 여유 공간입니다. 이 빈 공간은 좋은 것입니다.
버벅거림의 기타 원인
JavaScript 기반 애니메이션을 실행하려고 할 때 가장 큰 문제는 다른 항목이 rAF 콜백을 방해하여 실행되지 않을 수도 있다는 점입니다. rAF 콜백이 간결하고 몇 밀리초 만에 실행되더라도 다른 활동 (예: 방금 들어온 XHR 처리, 입력 이벤트 핸들러 실행, 타이머에서 예약된 업데이트 실행)이 갑자기 들어와서 실행되며, 이때는 실행 중간에 포기하지 않고 계속 실행됩니다. 모바일 기기에서는 이러한 이벤트를 처리하는 데 수백 밀리초가 걸릴 수 있으며, 이때 애니메이션이 완전히 중단됩니다. 이러한 애니메이션 끊김을 버벅거림이라고 합니다.
이러한 상황을 피할 수 있는 마법 같은 해결책은 없지만 성공을 위한 몇 가지 아키텍처 권장사항이 있습니다.
- 입력 핸들러에서 많은 처리를 하지 마세요. 많은 JS를 실행하거나 onscroll 핸들러와 같은 중에 전체 페이지를 재정렬하려고 하면 심각한 버벅거림이 발생하는 매우 일반적인 원인이 됩니다.
- 최대한 많은 처리 (실행하는 데 시간이 오래 걸리는 작업)를 rAF 콜백 또는 웹 워커로 푸시합니다.
- 작업을 rAF 콜백에 푸시하는 경우 각 프레임에서 조금씩만 처리하도록 분할하거나 중요한 애니메이션이 끝날 때까지 지연해 보세요. 이렇게 하면 짧은 rAF 콜백을 계속 실행하고 원활하게 애니메이션을 처리할 수 있습니다.
입력 핸들러가 아닌 requestAnimationFrame 콜백으로 처리를 푸시하는 방법을 다루는 유용한 튜토리얼은 폴 루이스님의 도움말 requestAnimationFrame을 사용한 더 가볍고 강력하며 빠른 애니메이션을 참고하세요.
CSS 애니메이션
이벤트 및 rAF 콜백에서 경량 JS보다 나은 것은 무엇인가요? JS가 없습니다.
앞서 rAF 콜백이 중단되지 않도록 하는 확실한 방법은 없다고 했지만 CSS 애니메이션을 사용하면 이러한 콜백이 전혀 필요하지 않게 됩니다. 특히 Android용 Chrome (및 기타 브라우저에서 유사한 기능을 사용 중)에서 CSS 애니메이션은 JavaScript가 실행 중인 경우에도 브라우저에서 자주 실행할 수 있다는 매우 바람직한 속성을 갖습니다.
위의 버벅거림 섹션에는 브라우저가 한 번에 한 가지 작업만 할 수 있다는 암시적 진술이 있습니다. 이는 엄밀히 사실은 아니지만 유용한 가정입니다. 언제든지 브라우저는 JS를 실행하거나 레이아웃을 실행하거나 페인팅을 실행할 수 있지만 한 번에 하나씩만 실행할 수 있습니다. 이는 DevTools의 타임라인 뷰에서 확인할 수 있습니다. 이 규칙의 예외 중 하나는 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의 이 도움말과 같은 도움말을 참고하세요.
마무리
요약하자면 다음과 같습니다.
- 애니메이션을 만들 때는 모든 화면 새로고침에 맞춰 프레임을 생성하는 것이 중요합니다. Vsync'd 애니메이션은 앱의 느낌에 큰 긍정적인 영향을 미칩니다.
- Chrome 및 기타 최신 브라우저에서 vsync 애니메이션을 사용하는 가장 좋은 방법은 CSS 애니메이션을 사용하는 것입니다. CSS 애니메이션보다 더 많은 유연성이 필요한 경우 가장 좋은 기법은 requestAnimationFrame 기반 애니메이션입니다.
- rAF 애니메이션을 원활하게 실행하려면 다른 이벤트 핸들러가 rAF 콜백 실행을 방해하지 않도록 하고 rAF 콜백을 짧게 유지합니다(15ms 미만).
마지막으로 vsync'd 애니메이션은 간단한 UI 애니메이션에만 적용되는 것이 아니라 Canvas2D 애니메이션, WebGL 애니메이션, 정적 페이지 스크롤에도 적용됩니다. 이 시리즈의 다음 도움말에서는 이러한 개념을 염두에 두고 스크롤 성능을 자세히 살펴봅니다.
즐겁게 애니메이션을 만들어 보세요.