DocumentFragment를 활용하여 Reflow를 줄이는 방법

정진원·2022년 10월 1일
0

Pro-Pro

목록 보기
1/1

Pro-Pro 프로젝를 회고하며 웹 성능 해결과 관련된 내용을 정리한다.

프로젝트 당시 페이지에 새로운 게시글 컴포넌트들을 생성할 때마다 appendChild 메소드로 부모 노드에 붙이는 방식이었다. 하지만, 이런 방식은 새로운 컴포넌트를 붙일 때마다 브라우저가 요소를 재배치한다. 개발 단계에는 많은 게시글을 보여주지 않기 때문에 큰 성능 차이가 보이지 않았지만, 배포 이후 사용자가 몰린다면 성능 저하로 직결되는 문제라 생각했다.

브라우저의 렌더링 과정을 되짚으며 문제가 일어난 단계와 해결방안을 고민하기로 했다. 이전에 읽었던 Naver D2의 “브라우저는 어떻게 동작하는가?” 포스팅이 생각나 이 글을 보며 어느 과정에서 문제가 발생했는지 고민했다.

브라우저의 기본 구조

  • 사용자 인터페이스 - 주소 표시줄, 이전/다음 버튼, 북마크 메뉴 등. 요청한 페이지를 보여주는 창을 제외한 나머지 모든 부분이다.
  • 브라우저 엔진 - 사용자 인터페이스와 렌더링 엔진 사이의 동작을 제어.
  • 렌더링 엔진 - 요청한 콘텐츠를 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함.
  • 통신 - HTTP 요청과 같은 네트워크 호출에 사용됨. 이것은 플랫폼 독립적인 인터페이스이고 각 플랫폼 하부에서 실행됨.
  • UI 백엔드 - 콤보 박스와 창 같은 기본적인 장치를 그림. 플랫폼에서 명시하지 않은 일반적인 인터페이스로서, OS 사용자 인터페이스 체계를 사용.

나는 컴포넌트를 새로 화면에 표시하는 작업에 문제가 생겼기 때문에 렌더링 엔진 부분에 주목해야 한다.

렌더링 엔진

  • 렌더링 엔진의 역할은 요청 받은 내용을 브라우저 화면에 표시하는 일이다. 렌더링 엔진은 HTML 및 XML 문서와 이미지를 표시할 수 있다.

다음은 렌더링 엔진의 전체적인 동작과정이다.

1. 렌더링 엔진은 HTML 문서를 파싱하고 콘텐츠 트리 내부에서 태그를 DOM으로 변환한다.

<html>
  <body>
   <p>Hello World</p>
   <div><img src="example.png" /></div>
  </body>
</html>  

다음과 같은 마크업에서 DOM 트리 그림은 아래와 같이 그려진다.

2. 외부 CSS 파일과 함께 포함된 스타일 요소도 파싱하여 CSSOM을 생성한다.

CSS 파일은 스타일 시트 객체로 파싱되고 각 객체는 CSS 규칙을 포함한다. CSS 규칙 객체는 선택자와 선언 객체 그리고 CSS 문법과 일치하는 다른 객체를 포함한다.

3. 이 둘을 어태치먼트(형상 구축)하여 스타일 정보와 HTML 표시 규칙을 포함한 렌더 트리(형상 트리)를 생성한다.

아래 그림은 Ucacity에서 The Render Tree - Website Performance Optimization영상의 일부다. 렌더트리 구축을 좀 더 시각적으로 표현해서 참고자료로 첨부했다.

4. 렌더 트리 노드들의 크기와 위치정보를 계산하여 배치한다.(Reflow)

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

위 코드를 기반으로 한 Render Tree에는 노드와 그 노드에 대한 스타일이 정의되어 있다. 하지만 실제 화면에서 어느 곳에 어떤 크기로 위치할지는 정해지지 않은 상태이다.

이를 위해 이 단계에선 Render Tree를 루트에서부터 탐색하여 실제 기기 화면의 크기에 맞춰 노드의 정확한 위치와 크기를 계산한다. 상대적이었던 측정값을 화면 안에서의 절대적인 픽셀로 변환하는 과정이다.

5. 계산된 위치정보를 기반으로 실제로 노드들을 그린다.(Repaint)

Render Tree를 순회하며 이미 Reflow에서 계산한 위치, 크기를 제외한 나머지 CSS 속성들을 적용하여 렌더 트리의 각 노드를 화면의 실제 픽셀로 변환한다.

브라우저에 변경사항이 생기면?

  • 브라우저는 변경에 대해 가능한 한 최소한의 동작으로 반응하려고 노력한다. 그렇기 때문에 요소의 색깔이 바뀌면 해당 요소의 리페인팅만 발생한다. 요소의 위치가 바뀌면 요소와 자식 그리고 형제의 Repaint와 Reflow가 발생한다. DOM 노드를 추가하면 노드의 Repaint와 Reflow가 발생한다.

나의 코드는 DOM 노드를 추가하는 코드였기 때문에 Reflow 단계에서 문제가 발생했다고 생각했다. 컴포넌트를 직접 부모 노드에 붙이다 보니 이를 재배치하기 때문이다. 성능을 개선하려면 Reflow를 줄여야 한다 생각해서 이를 해결하기 위한 방안을 고민했다.
특정 요소를 생성하여 컴포넌트들을 이 요소에 모두 붙인 뒤, 그 요소를 부모 노드에 붙인다면 해당 요소가 노드에 붙는 한 번의 Reflow만으로 페이지를 구현할 수 있다고 생각했다. 어떤 요소를 사용해야 좋을지 고민 도중 MDN에서 DocumentFragment라는 객체를 접했다.

DocumentFragment는 일반 문서처럼 노드로 구성된 문서 구조를 저장할 수 있지만 활성화된 문서 트리 구조의 일부가 아니기 때문에 내부의 트리를 변경해도 문서나 성능에 아무 영향도 주지 않는 객체다. 이는 해당 객체 내부에 다수의 컴포넌트를 붙여도 문서에 직접적인 영향을 주지 않고, 객체를 직접 문서에 붙여야 Reflow가 발생함을 의미한다. 이런 특성이 Reflow를 줄이기 위한 방안을 찾던 나에게 적합하다 생각해서 이를 활용해 코드를 리팩터링했다. 공식문서의 다음 코드를 참고했다.

const ul = document.querySelector('ul');
const fruits = ['Apple', 'Orange', 'Banana', 'Melon'];

const fragment = new DocumentFragment();

for (const fruit of fruits) {
  const li = document.createElement('li');
  li.textContent = fruit;
  fragment.append(li);
}

ul.append(fragment);

코드 리팩터링

리팩터링전 코드다.

  cardRender() {
    const cards = this.container.querySelector('.bookmark__cards');

    this.state.cards.forEach(item => {
      const card = createDom('div', {
        className: 'card-wrapper',
      });

      new Card({
        container: card,
        props: {
          type: 'bookmark',
          post: item,
        },
      });
      cards.appendChild(card);
    });
  }

크롬 성능테스트 결과는 다음과 같다.

DocumentFragment 객체를 하나 선언하고, 컴포넌트들을 직접 부모 노드에 붙이지 않고 이 객체에 붙인 뒤, 해당 객체를 부모 노드에 붙이는 방식으로 코드 스타일을 바꿨다.

  cardRender() {
    const cards = this.container.querySelector('.bookmark__cards');

    const frag = new DocumentFragment();

    this.state.cards.forEach(item => {
      const card = createDom('div', {
        className: 'card-wrapper',
      });

      new Card({
        container: card,
        props: {
          type: 'bookmark',
          post: item,
        },
      });
      frag.appendChild(card);
    });
    cards.appendChild(frag);
  }

크롬 성능테스트 결과는 다음과 같다.

개발 단계에서의 테스트라 사실 크게 눈에 띄는 개선은 없었다. 하지만, 기존에는 한 번도 고민 해본 적 없던 서비스 성능과 관련된 문제를 협업을 위해 도입했던 방법으로 해결하며 개발자로서 한 단계 성장할 수 있었던 경험이었다.

참고 사이트

https://d2.naver.com/helloworld/59361
https://twofivezero.tistory.com/56
https://web.dev/critical-rendering-path-render-tree-construction/
https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment

profile
깊이 있는 학습, 클린 코드, 의사소통

0개의 댓글