2023-05-05

모바일 환경에서 Animation 프레임 드랍 해결하기

2023-05-04에 수정한 Animation을 deploy해서 모바일 환경에서 구동해보았는데, 데스크탑 환경에서 보던 부드러움이 나오지 않는다. 보기 거북할 정도로 프레임 드랍이 일어나, 문제가 뭘까 생각해보고 해결해보려 한다.

new Date() 쓰지 마세요?

위 문제를 해결하기 위해 Stack Overflow를 뒤져보던 도중, 다음과 같은 글을 발견했다. Calculate FPS in Canvas using requestAnimationFrame

기존 코드에서 현재 FPS를 계산하기 위해서 new Date()를 사용하고 있는데, Date API는 인터벌을 측정하는 데에 적합하지 않다는 것이다.

The Date-API uses the operating system's internal clock, which is constantly updated and synchronized with NTP time servers. This means, that the speed / frezquency of this clock is sometimes faster and sometimes slower than the actual time - and therefore not useable for measuring durations and framerates.

If someone changes the system time (either manually or due to DST), you could at least see the problem if a single frame suddenly needed an hour. Or a negative time. But if the system clock ticks 20% faster to synchronize with world-time, it is practically impossible to detect.

Also, the Date-API is very imprecise - often much less than 1ms. This makes it especially useless for framerate measurements, where one 60Hz frame needs ~17ms.

위와 같은 이유 때문에, performance.now()를 사용하는 것이 좋다고 한다.

그래서, new Date()로 시간을 측정하던 부분을 performance.now()로 모두 리팩토링하였고, 작성된 소스코드는 다음과 같다.

const refAngle = useRef<number>(-0.4)
const [angle, setAngle] = useState<number>(-0.4)
var then: number
var startTime: number
var frameCount: number = 0

const animate = () => {
  if (refAngle.current > 1) return

  var now: number = performance.now()

  var sinceStart = now - startTime

  var currentFps = Math.round((1000 / (sinceStart / ++frameCount)) * 100) / 100

  refAngle.current += 1.25 / currentFps
  setAngle(refAngle.current)

  requestAnimationFrame(animate)
}

useEffect(() => {
  then = performance.now()
  startTime = then

  var animationFrame = requestAnimationFrame(animate)

  return () => {
    cancelAnimationFrame(animationFrame)
  }
}, [])

그런데, 위 글에 딸린 Comment들을 읽던 도중 다음과 같은 글을 발견했다.

Don't use the Date, but don't even use performance.now. requestAnimationFrame passes an highResTimestamp to its callback

requestAnimationFrame()이 콜백으로 주는 타임스탬프를 이용하는 것이 더 낫다는 것인데, 이 방법으로도 구현해보고 성능을 테스트 해볼 예정이다.

결과

위와 같이 new Date()performance.now()로 변경하는 것 만으로도 모바일 환경에서 프레임 드랍이 일어나던 문제가 어느정도 해결되었다. 그럼에도 불구하고, 60fps가 나오지는 않아서 눈으로 보기에는 여전히 부드럽지는 않은데, 이 부분은 또 어디가 문제인지 다시 확인해볼 생각이다.

관련 문제를 찾아보던 도중, iOS 자체에서 requestAnimationFrame()을 30fps로 Throttle을 건다는 글을 발견했다. 이와 관련된 문제인지 다시 한 번 살펴봐야겠다.


© 2023 Yulwon Rhee, Built with Gatsby