requestAnimationFrame와 passive:true로 스크롤 이벤트 최적화하기

성능 최적화 관련해서 공부하다보면 꼭 있는 내용이 requestAnimationFrame이다.

사용해 본 경험이 없어 사실상 죽은 지식이였는데, 이번 기회에 사용해보려고 공부를 해보니까 주로 스크롤 이벤트 최적화에 많이 사용한다고 한다.

스크롤 이벤트 최적화를 위해선 EventTarget.addEventListener()의 pasive 옵션과 requestAnimationFrame에 대한 이해가 필요하다.

passive

EventTarget.addEventListener() 함수의 세 번째 parameter인 options 객체에는 passive라는 옵션이 존재한다.

이는 이벤트 핸들러 내부에서 절대 preventDefault()를 호출하지 않을 것을 나타내는 boolean 값이다.

EventTarget.addEventListener()를 통해 등록된 이벤트는 컴포지터 스레드가 받는다. 이벤트가 발생하면 컴포지터 스레드는 메인스레드에 이벤트를 넘기고 reflow 또는 repaint가 발생하고 렌더링 파이프라인에 따라 리렌더링 되는 것이 일반적인 과정이다.

하지만 passive 값을 true로 할 경우 컴포지터 스레드에서 이벤트를 메인스레드에 넘기고 처리를 기다리지 않고 바로 Composite하여 새로운 프레임을 바로 합성하게 된다.

만약 핸들러 내부에 e.preventDefault()가 존재하여 이를 수행할 경우 메인스레드에서 해당 이벤트 발생시의 기본 동작을 막고 이벤트 핸들러를 수행해야 한다. 하지만 passive값을 true로 할 경우 e.preventDefault()가 핸들러 내부에 존재하지 않는다는 것이 보장되기 때문에 메인스레드의 처리를 기다리지 않고 바로 새로운 프레임을 합성할 수 있게 된다.

명시하지 않아도 최신 브라우저에선 문서 레벨 노드인 Window Document Document.bodytouchstart 이벤트와 touchmove에선 passive의 기본 값을 true로 적용하고 있다.

지원하는 브라우저인지 여부는 다음과 같이 확인할 수 있다.(mdn 출처)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 기능 감지 */
let passiveIfSupported = false;

try {
window.addEventListener(
"test",
null,
Object.defineProperty({}, "passive", {
get: function () {
passiveIfSupported = { passive: true };
},
})
);
} catch (err) {}

window.addEventListener(
"scroll",
function (event) {
/* do something */
// event.preventDefault() 사용 불가
},
passiveIfSupported
);

스크롤 이벤트 최적화하기

스크롤 이벤트를 최적화하는 방법에 대해 알아보자

최적화 x

아래는 최적화 없이 작성한 코드이다.

1
window.addEventListener("scroll", () => console.log("scrolled"));

위와 같은 이벤트 핸들러를 등록할 시 console.log가 계속 찍히게 된다.
console.log말고 reflow를 발생시키는 callback을 등록했을 경우 브라우저에는 더 많은 과부하가 발생하게 된다.

최적화 적용하기

아래는 최적화를 적용하여 작성한 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function optimizeHandler(callback) {
if (!callback) return;
let tick = false;
return function () {
if (tick) return;
tick = true;
requestAnimationFrame(() => {
callback();
tick = false;
});
};
}
window.addEventListener(
"scroll",
optimizeHandler(() => console.log("realcall")),
{ passive: true }
);

callback함수를 받아 최적화된 함수를 반환하는 optimizeHandler 함수를 작성하였다.

위의 코드는 다음과 같은 방식으로 작동한다.

  1. 클로저를 이용할 수 있도록 반환되는 함수 바깥에 tick을 선언한다.
  2. 반환되는 함수 내부에선 tick이 true일경우 별도의 작업을 하지 않고 얼리리턴한다.
  3. 반환되는 함수 내부에서 tick이 false일경우 requestAnimation을 호출하여 callback을 수행하고 tick을 true로 바꾼다.

Ref

{ passive:true } 의 진정한 의미

패시브 수신기로 스크롤 성능 향상

스크롤 등의 이벤트 최적화하기
스크롤 이벤트 최적화