requestAnimationFrame과 cancelAnimationFrame의 관계
Summary
Summary
`requestAnimationFrame`이 반복 호출될 때의 특성과, 종료 시점에 `cancelAnimationFrame`으로 정리하지 않으면 발생할 수 있는 성능/메모리 이슈를 정리합니다.
QA팀으로부터 현재 운영 중인 웹사이트 관련 리포트를 몇 건 전달받았습니다. 사이트를 탐색하다 보면 전체적으로 느려진다는 내용이었는데, 리포트들의 공통점은 특정 애니메이션을 실행한 이후부터 사이트가 느려진다는 점이었습니다.
해당 애니메이션은 loop되는 구조로, 특정 영역에 mouseEnter 하면 재생되었다가 mouseLeave 하면 애니메이션이 숨김 처리되는 형식이었습니다. 애니메이션을 재생한 상태에서 개발자 도구로 성능/메모리 상태를 살펴보니 Animation Frame Fired 항목이 가장 높은 점유율을 차지했고, 애니메이션이 화면에서 사라졌음에도 지속적으로 리소스를 점유한다는 사실을 확인했습니다.

그래서 애니메이션 최적화 방법을 찾아보니 requestAnimationFrame이 계속 언급되었고, 마침 운영 중인 사이트에서도 이를 사용하고 있길래 관심을 갖게 되었습니다. requestAnimationFrame이 무엇이고 이를 어떻게 정상적으로 종료해야 하는지 정리합니다.
requestAnimationFrame이란?
JavaScript의 내장 함수로, 다음 프레임에 실행할 애니메이션 콜백을 등록하고 브라우저가 적절한 시점에 호출하도록 하는 API입니다.
기존 setInterval과 달리 디스플레이 주사율에 맞춰 호출 시점이 조절되고, 백그라운드 상태에서는 호출이 제한(또는 중단)된다는 장점이 있습니다.
다만 유의해야 할 점이 있습니다. setInterval은 clearInterval로 타이머를 해제하듯이, requestAnimationFrame도 종료 조건이 없다면 cancelAnimationFrame으로 루프를 정리해야 합니다.
// 애니메이션 id를 저장할 변수
let id = 0;
...
function init(){
...
console.log("animation start!!");
animate();
}
...
// 매 프레임마다 id를 갱신한다.
function animate() {
console.log("animate...");
id = requestAnimationFrame(animate);
}
...
// 애니메이션 종료 시점에 호출하여 저장해둔 id를 인자로 전달합니다.
function destroy(){
console.log("animation end // animation id : ", id);
cancelAnimationFrame(id);
}
사용법은 간단합니다. requestAnimationFrame은 id를 반환하므로, 해당 id를 저장해두었다가 애니메이션을 종료해야 하는 시점에 cancelAnimationFrame(id)를 호출하면 됩니다.
cancelAnimationFrame 적용 비교
수정 전후를 같은 조건에서 비교하기 위해, 문제가 보고된 화면을 로컬 개발 서버로 띄워 놓고 측정했습니다. 마우스를 처음 10초 동안 mouseEnter 상태로 두어 애니메이션을 재생시키고, 다음 10초는 mouseLeave 상태로 두어 애니메이션이 숨겨진 뒤의 상태를 보는 방식입니다.
cancelAnimationFrame 함수 적용 전

cancelAnimationFrame 함수 적용 후

성능 수치 비교
위가 cancelAnimationFrame 적용 전이고, 아래가 적용 후입니다. 두 그래프 모두 가로축이 20초 구간이고, 가운데가 mouseLeave 한 시점입니다. 같은 조건에서 DevTools Performance 탭으로 측정한 결과입니다.
| 측정 항목 | 적용 전 | 적용 후 | 변화 |
|---|---|---|---|
| Animation Frame Fired CPU 점유율 (mouseLeave 후) | 41% | 3% | 93% 감소 |
초당 animate 함수 호출 횟수 (mouseLeave 후) | ~60회/초 (60fps 기준) | 0회/초 | 완전 중단 |
| 전체 페이지 Scripting 시간 (20초 기준) | 8.2초 | 4.1초 | 50% 감소 |
| 메모리 사용량 증가 추이 (mouseLeave 후 10초간) | 지속 증가 (~2~3MB) | 증가 없음 (안정) | 메모리 누수 해소 |
문제의 핵심은 mouseLeave 이후에도 requestAnimationFrame 루프가 멈추지 않는다는 것이었습니다. 적용 전 그래프를 보면 애니메이션이 화면에서 사라진 뒤에도 Animation Frame Fired가 CPU의 41%를 점유하고 있었고, 콘솔에서도 animate 함수가 초당 60회씩 계속 호출되고 있었습니다. 보이지 않는 애니메이션이 백그라운드에서 리소스를 계속 소모하고 있었습니다.
cancelAnimationFrame을 적용한 뒤에는 mouseLeave 시점에 루프가 즉시 끊기면서 CPU 점유율이 3%로 떨어졌고, animate 호출도 완전히 멈췄습니다. 메모리 쪽도 마찬가지입니다. 적용 전에는 콜백 참조가 해제되지 않아 10초간 2~3MB씩 증가하던 것이, 적용 후에는 안정적으로 유지되었습니다.
애니메이션이 사라진 후에도 계속 리소스를 점유하던 현상이 없어지면서, QA팀이 리포트했던 사이트가 느려지는 문제도 해결되었습니다.
샘플 코드
측정에 사용한 로직만 따로 떼어내, requestAnimationFrame의 동작을 바로 확인할 수 있게 정리해 둔 저장소입니다.