Reflow 와 Repaint - 성능 최적화

ChoiYongHyeun·2024년 2월 7일
0

브라우저

목록 보기
3/16
post-thumbnail

저번에 브라우저 렌더링 과정에 대해 공부해봤다
브라우저는 어떻게 렌더링 되는가
그럼 이제 JavscriptDOM 을 조작했을 때 다시 렌더링 되는 과정인 RepaintRepaint 에 대해 공부해보자


Render Tree 를 빠르게 초기에 구성 하는 법

기본적으로 내가 제공한 HTML , CSS 들을 User Interface 에 렌더링 시킨다는 것은

파싱하여 제작한 DOM , CSSOM 을 이용해 Render Tree 를 만들고 Render Tree 들을

정의된 위치 및 크기 속성을 이용해 노드들이 위치할 Layout 을 계산하고

정의된 픽셀 단위의 속성을 이용해 브라우저에 Paint 하는 것을 의미한다.

이전 강의에서도 보았지만 Render Tree 를 빠르게 구성하기 위해서는

복잡하지 않은 DOM , CSSOM 을 만드는 것이 필요하다.

특히 CSSOM 을 복잡하지 않게 만드는 것이 중요하다.

복잡한 CSSOM 이란 선택자를 복잡하게 표기한 것을 의미한다.

예를 들어

  <body>
    <article>
      <div>
        <p>
          <span> 안녕하세요 ~! </span>
        </p>
      </div>
    </article>
  </body>

다음과 같은 노드가 존재 할 때 선택자를

article > div > p > span {color: blue;} 처럼 구성하는 경우가 그렇다.

이런식으로 정의한 CSSOM

다음과 같이 depth 가 늘어나게 되는데

이는 Render Tree 를 구성 할 때 DOMspan 태그를 만날 때 마다 해당 span 태그가

article > div > p 의 관계를 갖고 있는지 위 노드까지 재확인 해야 하기 때문에 Render Tree 를 구성하는데 시간이 걸린다.

그래서 최대한 Render Tree 를 빠르게 구성하기 위해서는 CSSOMdepth 를 낮춰줄 필요가 있는데 이런 경우엔 식별자를 이용하는 것이 좋다.

  <body>
    <article>
      <div>
        <p>
          <span class="greet"> 안녕하세요 ~! </span>
        </p>
      </div>
    </article>
  </body>

다음처럼 스타일을 지정하고 싶은 태그에 식별자를 지정해주면

Render Tree 를 구성하기 위해 DOM 의 노드들을 순회 할 때 식별자만 확인하여

Render Tree 를 빠르게 구성 할 수 있기 때문이다.

식별자를 이용한다는 것은 효과적으로 Render Tree 를 빠르게 렌더링 하는 방법 중 하나이다.

나머지 방법들은 밑에서 실습을 통해 알아가도록 하자


JavascriptDOM 을 조작한다는 것

script 파일로 DOM 을 조작한다는 것은

앞서 제작한 Render Tree 에 새로운 노드를 추가하거나 기존 노드를 제거하거나

layout , paint 관련 속성을 수정하거나 식별자 (class , id) 들을 변경하는 행위를 말한다.

Render Tree를 조작한다는 것은 수정된 Render Tree 를 만들어 User interface 에 띄우겠다는 것이다.

layout , paint 의 일련의 과정을 거쳐서 말이다.


Render Tree 를 수정하면 어떤 일이 일어날까 ? - Dirty bit system

Dirty bit - Wikipedia

Dirty bit 의 사전적 의미란 메모리의 어떤 정보가 수정되었을 때 모든 메모리의 정보를 순회하며 수정하는 것이 아니라

해당 메모리와 관련있는 메모리의 비트들만 수정 하는 것을 의미한다.

Render Tree 를 수정하는 것도 Dirty bit system 을 이용한다.

어떤 노드가 변경 되었을 때 모든 Render Tree 들을 layout , paint 과정을 거쳐 재렌더링 하는 것은 비효율적이다.

그렇기에 Render Tree 가 수정이 된다면 수정된 노드에게 직접적인 영향을 받는 요소들까지 변경하여 재렌더링 한다.


ReflowRepaint

Reflowlayout 관련 속성이 변경되어 브라우저에 노드들의 위치를 새로 계산하는 과정을 의미하고

Repaint 는 브라우저에서 노드들의 시각적인 요소를 변경하여 렌더링 하는 과정을 의미한다.

우리가 JavascriptDOM 조작을 하는 행위들은 대부분 ReflowRepaint 를 일으키는 행위들이다.

브라우저에서 노드들이 지속적으로 다른 모습으로 렌더링 됨으로서 인터렉티브한 경험을 주고자 하기 때문이다.

"가끔씩 reflow 는 나쁘고 repaint 는 나쁘지 않다" 라는 말을 심심찮게 들어볼 수 있었을 것이다.

그런 말이 나오는 이유는 reflow 의 경우 기하학적 요소가 변경되면 영항을 받는 노드들이 늘어날뿐더러 필연적으로 reflow 가 일어나면 repaint 도 같이 일어나기 때문이다.

렌더링 과정의 일련적 과정을 기억하자

reflow 가 영향을 미친다는 것들의 의미를 예시를 통해 확인해보자


ReflowLayout 을 재구성 하는 것

노드들의 기하학적 속성을 계산하는 Layout 과정에서 살펴보는 스타일 속성들은 다음들과 같다.

모든 스타일 속성들을 이야기 할 수는 없으나 예시 몇 가지를 들으면 어떤 것인지 이해가 될 것이다.

display , top , right , bottom , left , margin , padding , font-size , font-family , width , height ..

들과 같이 노드가 브라우저에 위치할 곳에 영향을 미치는 요소들을 의미한다.

크기 관련 속성들도 layout 에 영향을 미치는 이유는 특정 태그의 크기에 따라

위치가 변경되는 태그들이 필연적으로 존재하기 때문이다.

다음과 같은 예시를 살펴보자

  <body>
    <div class="parent">
      <div class="child"></div>
      <div class="sibling"></div>
    </div>
  </body>

다음과 같이 child 태그를 클릭하면 width , height 만 변경되도록 이벤트 핸들러를 등록해보았다.

child 태그의 width , height 만 변경시켰을 뿐인데 부모 태그의 너비도 함께 늘어나고

형제태그인 sibling 태그의 위치가 변경되는 모습을 볼 수 있다.

이처럼 layout 이 변경되면 자신에게 영향을 받는 노드들의 layout 값도 모두 변경해야 하기 때문에 비용이 높은 액션에 해당한다.

개발자 도구 - performance 로 살펴본 Reflow

지속적으로 child 노드의 크기가 변경되는 여러 프레임 중 한 프레임을 가져와봤다.

Recalculate StyleRender Tree 에서 해당 노드의 width , height 값을 변경하는 것을 의미한다.

layout값과 관련된 속성을 변경했으니 각 노드들의 layout 값을 다시 계산하는 layout 과정을 거치고

이후 paint , commit 과정을 통해 브라우저에 렌더링 된다.

이처럼 layout 관련 속성이 변경되면 영향을 받는 노드들의 layout 관련 속성을 계산하는 과정이 필연적으로 나타난다.

🤔 다른 노드들이 영향을 받지 않더라도 layout 과정은 필연적으로 발생할까 ?

이번에는 아예 부모 태그의 width , height 를 절대적으로 지정해주고 child 노드의 크기를 변경시켜보았다.

child 노드의 크기 변경에 상관 없이 부모 태그의 크기를 절대적으로 고정시켜뒀단 것이다.

그러면 어떻게 될까 ? 부모태그는 영향을 안받을 것이니 layout 단계가 발생 안하지 않을까 ?

발생한다.

또한 layout 까지 걸리는 시간이 줄어들지 않을까 했는데 동일하였다.

이러한 이유가 발생하는 이유는 변화 유무에 상관없이 layout 변경으로 인해 발생하는 reflow 과정에서

상위 태그 레이아웃에 잠재적인 영향을 미칠 수 있기 때문에 직접적으로 영향을 받을 것이라 예상되는 노드들의 layout 을 모두 순회하며 계산하기 때문이다.

그러니 변화 유무에 상관없이 reflow 는 관련 노드의 모든 layout 을 둘러본다는 것이다.


Reflowlayout 계산 시간을 단축시키는 방법

해당 방법들은 모두 transtion : .. 스타일 속성을 사용할 때를 가정한다.
inline style 을 1px씩 늘리는 것보다
inline style 한 번에 n px 로 늘리고 transition 속성을 사용하는 것이
훨씬 효율적이다.

그럼 reflowlayout 계산에 걸리는 시간을 단축 시키려면 어떻게 해야 할까 ?

그것은 영향을 받는 노드들을 최소화 시키는 것이다.

특정 노드의 layout 속성이 변경되면 다른 노드들의 layout 이 영향을 받는 이유는 DOMstatic relationship 을 갖기 때문이다.

우리가 DOM 을 구성하면 DOM은 계층적 구조를 가지며 각 태그들은 계층적 구조의 영향을 받아 문서 흐름에 맞게 포지션이 설정되는 것을 볼 수 있다.

부모 태그 내부에서 div 태그들은 차곡차곡 쌓이는 것 처럼 말이다.
기본적으로 태그들의 position 속성이 static 으로 설정되는 것을 생각해보자

이러한 계층적 구조때문에 layout 을 변경하면 계층적 구조의 노드들을 탐색하며 모든 layout 을 재구성 하는 것이다.

그럼 우리가 해야 할 일은 layout 의 변경이 다른 노드들에 영향을 미치지 않게 문서 흐름에서 벗어나도록 하는 것이다.

문서의 흐름에서 벗어나게 할 수 있도록 position 을 흐름에 맞게 배치시키는 것이 아니라

position : absoluteposition : fixed 로 고정시켜보자

이벤트 핸들러를 변경해주었다.

const $child = document.querySelector('.child');
document.addEventListener('DOMContentLoaded', () => {
  $child.style.width = '300px';
  $child.style.height = '300px';
});

최대한 다른 영향이 없도록 브라우저에 렌더링 되는 즉시 크기와 높이를 변경시켜주었다.

position : staticposition : absolutelayout 시간 비교

시간을 비교하기 앞서 모두 transtion : all 5s 로 설정해주고 동일한 이벤트 핸들러를 등록해주었다.

변경되는 것은 position 속성값 뿐이다.

position : static

static 일 경우 전체 layout 계산에 걸린 시간은 14.5ms14.5ms 이다.

position : absolute

positionabsolute 로 변경해주고 layout 계산에 걸린 시간은 12.4ms12.4ms 이다.

이러한 결과가 나타나는 이유는 앞서 말했듯 position : absolute 등으로 설정하게 되면 layout 을 다른 노드들에게 영향을 미치지 않았기 때문이다.

reflow 를 발생시키고자 한다면 최대한 다른 노드들의 layout 에 영향을 미치지 않도록

설정해주어야 한다.


Repaint

Repaint 는 비쥬얼 관련 속성이 변경되었을 때 브라우저에 다른 모습으로 변경되는 것을 말한다.

이 또한 변경이 된 노드들에 대해서만 변경이 일어나고

시각적 속성은 다른 노드들의 layout 에 영향을 미치지 않기 때문에 리소스 집약적이다.

그러니 Reflow 보다는 비용 측면에서 효율적이라는 것이다.

Repaint 만 일으키도록 DOM 을 조작해보자

const $child = document.querySelector('.child');
document.addEventListener('DOMContentLoaded', () => {
  const colorsArray = ['pink', 'red', 'green'];
  const maxInterval = 30;
  let curInterval = 0;
  const interval = setInterval(() => {
    $child.style.backgroundColor = colorsArray[curInterval % 3];

    if (curInterval++ > maxInterval) clearInterval(interval);
  }, 500);
});

setInterval 로 구현되어 타이머가 발화되면 Render Tree 에서 스타일 속성을 변경하는

Recalculate Style 이 일어나고

그 이후에는 Paint => Commit 만 일어난 모습을 볼 수 있다.

Layout 을 계산하는 과정은 없다.

그럼 어떻게 해야 Repaint 를 보다 효과적으로 할 수 있을까 ?

Repaint 자체를 최소화 하자

const $child = document.querySelector('.child');

$child.style.backgroundColor = 'red';
$child.style.border = '2px solid white';
$child.style.boxShadow = '0px 0px 10px 10px white';
$child.style.borderRadius = '20px';

다음처럼 개별적인 스타일 요소들을 한 코드에 한 줄씩 변경하는 것보다

$child.style.cssText = 'background-color: red; border: ... box-shadow: 0px 0px 10px 10px white;';

다음처럼 cssText 속성(인라인 스타일)을 한 번에 변경함으로서 Repaint 횟수를 줄일 수 있다.

$child.className += 'shadowBox'

더 좋은 방법은 식별자를 추가하거나 삭제하는 것으로 더 관리를 용이하게 할 수 있다.

킥.. 그런데 개발자도구에서 확인하니 큰 차이가 없더라

실제로 방법에 차이가 있는지를 확인해보려고 개발자 도구를 켜봤더니 차이는 없었다
이는 브라우저 엔진이 Repaint 계산을 최소화 하기 위해 다양하게 최적화 하기 때문이라고 한다.

스타일 변경사항을 일관 처리 하거나 스타일 변경의 패턴을 그룹화 하여 최적화 하거나
Reflow , Repaint를 일으키는 코드들을 수집한 후 한 번에 평가한다. (지연 평가)


더 많은 내용들은 브라우저 엔진이 어떻게 돌아가는지를 더 알고 나서 포스팅하려고 한다 :)

더 다양한 내용들을 담고 있는 아티클을 함께 첨부하도록 하겠다 !
Understanding Reflow and Repaint in the browser
CSS will-change 프로퍼티에 관해 알아둬야 할 것

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글