"다음 화면을 그리기 직전에 이 함수를 실행해 달라"고 브라우저에 예약하는 API. 콜백은 태스크 큐도 마이크로태스크 큐도 아닌 별도 목록에 들어가고, 이벤트 루프의 렌더링 단계에서 페인트 직전에 한꺼번에 실행된다.
- 디스플레이 주사율에 맞춰 프레임당 한 번 호출된다(60Hz면 약 16.7ms마다, 120Hz면 더 자주). 콜백 인자로 타임스탬프(
DOMHighResTimeStamp)를 받는데, 같은 프레임에서 실행되는 콜백은 모두 같은 값을 받는다 - 여기서 바꾼 DOM은 같은 프레임의 렌더링에 바로 반영된다. 시각적 변경을 모아 두기 좋은 곳이라 레이아웃 스래싱을 피할 때도 쓴다
- 콜백 안에서 다시
requestAnimationFrame을 부르면 다음 프레임으로 예약된다. 브라우저가 현재 프레임의 목록을 떠 놓고 실행하기 때문이고, 애니메이션 루프는 이 성질로 만든다 - 탭이 안 보이면 멈추거나 크게 늦춘다.
setTimeout은 백그라운드에서도 (느리게) 돈다. 화면 갱신이 목적이면 rAF를, 시간 기반 로직이면 타이머를 쓴다 - 메인 스레드에서 실행되므로 무거운 일을 하면 그만큼 페인트가 밀려 INP(Interaction to Next Paint)가 나빠진다. 그릴 시점을 맞춰 줄 뿐 일을 줄여 주지는 않는다
function tick(t) {
el.style.transform = `translateX(${(t / 10) % 300}px)`
if (running) requestAnimationFrame(tick) // 매 프레임 재예약
}
requestAnimationFrame(tick)멈추지 않는 rAF 루프
캐러셀 자동 회전이나 패럴랙스처럼 매 프레임 자신을 다시 예약하는 루프는, 화면에 아무 변화가 없을 때도 초당 60번씩 렌더링 단계를 깨운다. 한 번 한 번은 가벼워서 버벅임은 없어도 CPU(Central Processing Unit)·GPU(Graphics Processing Unit)가 절전 상태로 내려가지 못해 모바일 배터리를 갉아먹는다. 사용자가 가만히 있을 때 rAF 호출 수를 세어 보면 바로 드러난다. 정상이라면 0에 가깝다. 할 일이 없으면 재예약을 멈추고, 화면 밖이면 content-visibility의 상태 이벤트나 IntersectionObserver로 루프를 끈다.