반응형

이 글은 사용자 입력(input)을 받았을 때 컴포지터가 어떻게 부드러운 상호작용을 하는지에 관한 글이다

(출처는 https://developers.google.com/web/updates/2018/09/inside-browser-part4)

 

Inside look at modern web browser (part 4)  |  Web  |  Google Developers

Input event handling with the compositor thread

developers.google.com

 

사용자 입력을 받았을때

 

브라우저 관점에서 입력이란 모든 사용자의 제스처(이벤트)를 의미한다. 스크롤도 이벤트이고, 화면 터치, 마우스 포인터를 올리는거도 입력 이벤트이다.

 

 

화면 터치와 같은 사용자 제스처가 발생했을 때 가장 먼저 제스처를 수신하는 것은 브라우저 프로세스이다. 브라우저 프로세스는 제스처가 어디에서 발생했는지만 알고 있다. 탭 내부의 콘텐츠는 렌더러 프로세스가 처리해야 한다. 

 

그래서 브라우저 프로세스는 이벤트 종류와 좌표값(x,y 등)만 렌더러 프로세스에 넘기게 된다.

렌더러 프로세스는 이벤트 대상과 이벤트 리스너를 실행해 이벤트를 적절히 처리한다.

 

스크롤의 예시로 렌더러 프로세스 내부동작을 알아보자.

 

그림 출처 : https://developers.google.com/web/updates/2018/09/inside-browser-part4#compositor_receives_input_events

 

스크롤할때 레스터화된 레이어를 컴포지터가 합성해 스크롤을 부드럽게 처리한다.

이벤트리스너가 없다면 컴포지터 쓰레드는 메인 쓰레드와 상관없이 스스로 합성해서 프레임을 만들어낼 수 있다.

 

그런데 이벤트가 달려있다면 다르게 동작하게 된다.

합성하는 쓰레드는 컴포지트 쓰레드이고, 

이벤트를 실제로 처리하는 쓰레드는 JS를 동작시키는 메인 쓰레드이다.

어떻게 둘이 상호작용을 하게 될까?

 

고속 스크롤 불가 영역(non-fast scrollable region)

 

JS가 실행되는 작업은 메인 스레드의 영역이므로 이벤트 핸들러가 연결된 영역을 고속 스크롤 불가 영역(non-fast scrollable region)이라고 표시한다. 이 영역에서 이벤트가 발생하면 렌더러 프로세스는 우선 컴포지터 스레드에게 작업을 보내고, 컴포지트 스레드는 입력 이벤트를 메인 스레드로 보내야하는지 정보를 확인 후 넘겨준다.

 

만약 고속 스크롤 불가 영역이 아니라면 컴포지터 스레드는 메인 스레드를 기다리지 않고 새 프레임을 합성한다.

(컴포지터 스레드는 복사된 자신만의 레이어 트리를 가지고 있는다. - pending tree -> active tree?)

 

그렇다면 여기서 한가지 의문점을 가지게 된다.

이벤트 위임을 설정할때는 어떻게 될까?

 

ex)

document.body.addEventListener('touchstart', event => {  
    if (event.target === wantedNode) {
        event.preventDefault();
    }
});

이벤트 위임을 document나 root 등 최상위에 선언했을때

이벤트위임을 최상위에 선언했을경우 다음 그림과 같이 페이지가 전부 고속 스크롤 불가 영역에 속하게 된다.

 

이런 문제를 방지하기 위해 passive : true 옵션을 전달하면 메인스레드에서 이벤트를 받지만, 컴포지터가 메인 스레드의 처리를 기다리지 않고 새 프레임을 만들어도 되는 힌트를 브라우저에 주는 옵션을 줄 수 있다.

ex)

document.body.addEventListener('touchstart', event => {  
    if (event.target === wantedNode) {
        event.preventDefault();
    }
}, {passive: true});

 

JS로 가로로만 스크롤을 제한하고 싶을때 

passive : true 옵션을 주면 부드럽게 스크롤이 된다는거까지는 알았다. 그런데 스크롤을 JS의 event.preventDefault() 함수로 제한한다면 쓰레드가 다르므로 event.preventDefault() 가 실행되기 전 이미 수직 스크롤이 실행될 수도 있다. 그래서 event.cancelable()를 사용하면 스크롤 시작 여부를 확인 가능하다.

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // block the native scroll
        /*
        *  do what you want the application to do here
        */
    }
}, {passive: true});

 

======

 

역주 : 모질라에 따르면 passive : true는  event.preventDefault()가 내부에서 실행이 안될꺼라는 약속이라는데

위 코드를 실제로 실행해보면 에러가 뜬다.

 

아래 링크 참고

https://developer.mozilla.org/ko/docs/Web/API/EventTarget/addEventListener

위 코드를 복붙해서 실행했을때 나온 오류

https://stackoverflow.com/questions/37721782/what-are-passive-event-listeners

 

What are passive event listeners?

While working around to boost performance for progressive web apps, I came across a new feature Passive Event Listeners and I find it hard to understand the concept. What are Passive Event Listene...

stackoverflow.com

스텍오버플로우에 따르면

By marking a touch or wheel listener as passive, 
the developer is promising the handler won't call preventDefault to disable scrolling. 
This frees the browser up to respond to scrolling immediately without waiting for JavaScript, 
thus ensuring a reliably smooth scrolling experience for the user.

라는데 원글의 예시 코드는 이해를 돕기 위한 '예시'인지 좀 혼란스럽긴 하다.. 

일단 스택오버플로우와 원글의 공통점을 추론해보자면

passive : true를 설정하면 메인 쓰레드(JS)가 작동하지 않고 스크롤 이벤트때 컴포지트 쓰레드가 바로 동작하는 개념은 맞는듯 싶다.

 

======

 

(아무튼 계속 시작)

 

또는 아에 touch-action에서 아예 수직 스크롤 이벤트를 지워버릴수도 있다.

#area {
  touch-action: pan-x;
}

 

이벤트 대상 찾기

 

컴포지터 쓰레드가 메인 스레드로 보낼때 이벤트 대상을 찾는 hit test를 한다.

이벤트가 발생한 좌표에 무엇이 있는지 확인을 위해 paint recoards data를 이용한다.

(브라우저의 렌더링 단계에서 파싱->스타일->layout->paint->composite 에서 paint)

 

(아마 opcacity, transform등의 정보를 따로 담고 있는 property tree의 정보도 참고하기 위해서

paint tree를 참고하는거로 추론?)

 

메인 스레드 이벤트 전송 최소화

 

일반적으로 모니터는 1초당 60번 (60프레임) 화면을 갱신한다. 그리고 애니메이션도 화면 갱신 주기에 맞춰야

부드럽게 움직인다.

 

일반적으로 touchEvent는 초당 60~120회 전달하고, 마우스는 초당 100회정도 전달한다.

즉, 화면 갱신보다 이벤트 갱신 주기가 더 짧아서 이벤트가 뚝뚝 끊겨보이는 상황을 연출할 수도 있다.

 

너무 과도한 호출로 버벅거림을 유발하는 이벤트

 

그래서 메인 호출이 과다해지는것을 막기 위해서 Chrome은 자체적으로 연속적(coalesces continuous)인 이벤트(wheel , mousewheel , mousemove , pointermove , touchmove)는 다음번 requestAnimationFrame() 메서드 실행 직전까지 전송하지 않고 기다리게 된다.

 

keydown, keyup, mouseup, mousedown, touchstart, touchend와 같은 비연속적인(discrete) 이벤트는 즉시 전달된다.

 

한 프레임 안에서 합쳐진 이벤트 정보를 얻기 위해 getCoalescedEvent() 사용

드로잉 앱 같은 경우 프레임 사이 사이 경로가 누락되 선을 매끄럽게 그리지 못할 수도 있다. 이때 getCoalesecedEvents를 사용하면 합쳐진 이벤트의 정보를 얻을 수 있다.

 

 

 

 

reference 

 

https://d2.naver.com/helloworld/6204533

https://developers.google.com/web/updates/2018/09/inside-browser-part4

 

Inside look at modern web browser (part 4)  |  Web  |  Google Developers

Input event handling with the compositor thread

developers.google.com

 

반응형

+ Recent posts