본문 바로가기

FrontEnd

프론트엔드 성능 최적화 : layout thrashing 피하기 with requestAnimationFrame

반응형

requestAnimationFrame API는 왜 중요한가요?

불필요한 레이아웃을 피하는 방법과, 불가피하다면 성능 영향을 줄이는 방법을 알아봅니다.

 

아래 글과 이어서 보면 좋습니다.

https://itchallenger.tistory.com/838

 

브라우저의 이벤트 루프와 렌더링의 관계에 대해 알아보자

브라우저의 이벤트 루프와 렌더링은 어떤 관계가 있을까요? 원문 링크 : https://xnim.me/blog/javascript-browser-event-loop-layout-paint-composite-call-stack https://xnim.me/blog/javascript-browser-event-loop-layout-paint-composite-c

itchallenger.tistory.com

Layout Thrashing

Layout Thrashing은 프레임 몇 개가 누락되어, 화면 렌더링이 버벅이는 것처럼 느껴지는 현상을 의미합니다.

  • 보통 브라우저는 60fps로 화면을 업데이트 하는데, 각 화면 간의 업데이트 주기(약 16.6ms)가 달라진다면, 사용자는 렉을 느끼게 됩니다.
  • 레이아웃 스래싱은 페이지가 '로드'되기 전에 웹 브라우저가 웹 페이지를 여러 번 리플로우하거나 다시 그려야 하는 경우 발생합니다.
Layout Thrashing은 JavaScript가 DOM을 과도하게 읽고 쓸 때 발생하며, document 리플로우를 발생시킵니다.
// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers layout)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers layout)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';
DOM 객체가 write되면, 레이아웃이 '무효화'되며,
따라서 어느 시점에서 리플로우가 필요합니다.
브라우저는 리플로우를 최대한 지연하여 배치 처리하려 합니다.
60fps의 화면 업데이트 주기 보장만 가능하면, 굳이 불필요하게 많이 할 필요가 없기 떄문이죠.
즉, 현재 연산(또는 프레임 생명주기)이 끝날 때까지 기다리려고 합니다.
 

그러나 현재 연산(또는 프레임)이 완료되기 전에 DOM에서 기하학적 값을 다시 요청하면

(write 후 read 요청 - 동일 프레임 생명주기)
브라우저가 레이아웃을 일찍 수행하도록 강제합니다.
이는 'forced synchonous layout(강제 동기 레이아웃)'으로 알려져 있으며 성능을 저하시킵니다!

 

최신 데스크톱 브라우저에서 레이아웃 스래싱의 부작용이 항상 분명한 것은 아닙니다.
그러나 저전력 모바일 장치에는 심각한 결과가 있습니다.


이상적인 해결 방법

이상적으로 우리는 DOM 읽기와 DOM 쓰기를 함께 일괄 처리할 수 있도록 실행 순서를 간단히 재정렬 합니다.
이 경우 우리는 문서를 한 번만 리플로우하면 됩니다.

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame

이상과 현실의 괴리

실제로 이것은 그렇게 간단하지 않습니다.
대형 애플리케이션에는 곳곳에 코드가 흩어져 있으며,
이 모든 것에는 위험한 DOM 조작 연산이 있을 수 있습니다.
우리는 실행 순서를 제어할 수 있도록 분리된 코드들을 쉽게 하나로 합칠 수 없습니다.(물론 그렇게 해서도 안 됩니다).
최적의 성능을 위해 읽기 및 쓰기를 일괄 처리(배칭)하려면 어떻게 해야 합니까?

requestAnimationFrame 사용하기

window.requestAnimationFrame은 setTimeout(fn, 0)과 유사하게 다음 프레임에서 실행될 함수를 예약합니다.
  • 모든 DOM 쓰기를 다음 프레임에서 함께 실행되도록 예약하는 데 사용할 수 있기 때문에 매우 유용합니다.
  • 모든 DOM 읽기는 현재 사이클의 태스크 큐에서 실행되도록 남겨둡니다.
// Read
var h1 = element1.clientHeight;

// Write
requestAnimationFrame(function() {
  element1.style.height = (h1 * 2) + 'px';
});

// Read
var h2 = element2.clientHeight;

// Write
requestAnimationFrame(function() {
  element2.style.height = (h2 * 2) + 'px';
});
즉, 멋지게 캡슐화된 코드를 그대로 유지하고 약간의 코드 조정으로 값비싼 DOM 액세스를 일괄 처리할 수 있습니다.

실제 예제

주의 : reset 함수 때문에 벤치마킹이 이상할 수 있으니 반드시 새로고침 후 첫번째 결과만 확인하세요

아래 참고 글의 예제(working example)를 사용해 벤치마킹을 진행해 보겠습니다.

저는 새로고침 후 with layout thrash 버튼을 클릭하니

  • 레이아웃이 각 돔 조작 이터레이션 마다 수행되었으며
  • 레이아웃 시프트가 레이아웃 도중 지속적으로 발생했습니다.

매 이터레이션 별 레이아웃이 발생하는 모습

requestAnimationFrame 함수를 사용하면 어떻게 될까요?

  •  var width = div.clientWidth 라인 때문에 스타일 재계산이 일어나지만, 레이아웃이 매 이터레이션 마다 발생하지 않습니다.
    • 브라우저는 똑똑하기 때문에 해당 값이 조회된 이후로 변화된 점이 없으면 스타일 재계산을 다시 수행하지 않고 값을 리턴합니다.
  • 스타일 재계산은 렌더링 주기에서 함께 일어나도록 스케쥴 됩니다.

버튼 클릭 시 스타일 재계산만 수행

그리고 브라우저는 우리가 원하는 대로 레이아웃을 배치 처리합니다.

레이아웃이 배치 처리된 모습


참고 

https://blog.wilsonpage.co.uk/preventing-layout-thrashing/

https://stackoverflow.com/questions/34698433/what-is-involved-in-chromes-recalculate-style-event

https://gist.github.com/paulirish/5d52fb081b3570c81e3a

반응형