밝을희 클태

탁구채 애니메이션 최적화: setInterval vs requestAnimationFrame, position vs transform 본문

PongWorld 프로젝트

탁구채 애니메이션 최적화: setInterval vs requestAnimationFrame, position vs transform

huipark 2024. 4. 5. 16:00

문제

- 게임 플레이시 프레임 드랍이 일어남

원인

- setInterval() 사용 : 기존에 아래와 같이 60FPS설정을 setInterval()로 구현을 했다. 이 방법은 JavaScript 이벤트 루프 내에서 비동기적으로 실행되며 정확한 타이머주기를 보장하지 않는다. 지연이 누적되다 보면 프레임 드랍이 생긴다.

  setInterval(() => {
    if (isMovingUp) {
      coor = Math.max(
        parseInt(style.top) - 30,
        this.myPingpongStick.offsetHeight / 2,
      );
      this.update(coor);
    } else if (isMovingDown) {
      coor = Math.min(parseInt(style.top) + 30, this.maxY);
      this.update(coor);
    }
  }, 1000 / 60); // 60프레임으로 설정

- 리렌더링 : 탁구채의 위치를 변경할때 top, botton, left, right 등 position CSS 속성를 사용한다. 해당 속성은 다른 엘리먼트에 영향을 끼치기 때문에 리플로우, 리페인트가 발생하게 된다. 탁구채가 60프레임으로 움직이는데 1초에 60번씩 새로 화면을 그리게 되면서 환경에 따라 버벅이는 현상이 생긴다.

해결

- setInterval()대신 requestAnimationFrame()을 사용 : 브라우저가 렌더링 될 타이밍에 맞춰서 미리 함수를 예약을 해서 렌더링 주기에 맞는 부르더운 애니메이션을 구현 할 수 있다.

- position말고 transform속성을 이용 : 개발자 도구로 position과 translate를 각각 사용해서 성능을 확인해보면 position은 레이아웃 변경과 더불어 리페인트, 리플로우가 많이 일어난 것을 확인 할 수 있다. 그에 반해 translate는 레이아웃 변경 없이 깔끔한 상태를 볼 수 있다.

 

그리고 translate는 GPU를 사용하는데 position은 연산이 많아져도 CPU로만 연산을 수행해서 과부하가 올 수 있지만 translate는 연산을 할때 GPU를 사용한다 GPU는 그래픽 처리에 특화된 하드웨어로, 병렬 처리 능력이 뛰어나 많은 양의 계산을 빠르게 처리할 수 있다.

좌) position을 사용한 경우                                                                      우) translate을 사용한 경우

최종 코드 : 

animatePaddleMovement() {
    if (!this.rAF) {
      const animate = () => {
        if (isMovingUp || isMovingDown) {
          const myStickRect = this.myPingpongStick.getBoundingClientRect();

          if (isMovingUp) {
            if (myStickRect.top - this.speed <= this.tableRect.top) {
              this.currentPosY =
                this.currentPosY - (myStickRect.top - this.tableRect.top);
            } else this.currentPosY -= this.speed;
          } else if (isMovingDown) {
            if (myStickRect.bottom + this.speed >= this.tableRect.bottom) {
              this.currentPosY =
                this.currentPosY + (this.tableRect.bottom - myStickRect.bottom);
            } else this.currentPosY += this.speed;
          }

          this.update();
          this.myPingpongStick.style.transform = `translateY(${this.currentPosY}px)`;
          this.rAF = requestAnimationFrame(animate.bind(this));
        } else {
          cancelAnimationFrame(this.rAF);
          this.rAF = null;
        }
      };
      this.rAF = requestAnimationFrame(animate.bind(this));
    }
  }