Javascript/성능

[Javascript - 성능] DOM 렌더링 방식에 따른 성능 비교해보기 - 2

맨날개발 2025. 6. 29. 11:25
append / appendChild / DocumentFragmenet 성능 비교

 

테스트에 사용되는 코드

더보기
<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <div id="app"></div>

  <script>
    const count = 1000;

    function createElement(i) {
      const element = document.createElement('div');
      element.textContent = `${i}번째 div`;
      return element;
    }

    function appendChild() {
      const $app = document.getElementById('app');

      const start = performance.now();
    
      for (let i = 0; i < count; i += 1) {
        const element = createElement(i);
        $app.appendChild(element);
      }
    }

    appendChild();
  </script>
</body>
</html>

 

😎 지난 결론 다시보기

반복문 내에서 appendChild 호출을 통해 DOM 트리를 수정하더라도 즉시 리플로우(레이아웃)이 발생하지 않고 반복문이 종료된 후에 한번만 리플로우가 발생하였다.

 

이전에는 DOM 트리가 변경될 때마다 매번 리플로우가 발생해서 성능에 좋지 않은 영향을 주기 때문에, 변경사항을 최대한 줄일 수 있도록 해야 한다고 알아왔었다 

 

위의 결과에서 알 수 있듯이 브라우저는 DOM 변경 사항에 대해서 기록만 하고 있다가 자바스크립트의 실행이 종료된 후 한번에 결과를 리플로우하도록 최적화를 하고 있다.

 

 

🎈 리플로우 발생 시점

리플로우가 발생하는 시점은 다음과 같다. 크게 두가지로 나눠볼 수 있다.

  • 자바스크립트의 실행이 종료 된 시점 (자연스럽게 호출 되는 레이아웃)
  • 강제 레이아웃

 

첫 번째, 자바스크립트의 실행이 종료 된 시점에 렌더 큐에 변경사항이 존재하면 레이아웃이 발생한다. append와 같은 작업이 발생하면 렌더 큐에 추가되었다가 자바스크립트의 실행이 종료 된 후에 자연스럽게 레이아웃이 발생한다.

 

여기서 자바스크립트의 종료 시점이라는 것은, 콜스택이 모우 비어지고 마이크로 태스크 큐의 작업이 모두 실행 되고 큐가 비어진 이후 시점이라고 볼 수 있다. 이후 렌더 큐에 쌓인 작업에 의해 렌더링이 발생된다.

 

 

아래의 코드는 append 호출 후 돔 조작 이외에 로직 호출을 하고 마지막으로 Promise를 통한 마이크로 태스크 큐에 작업을 추가하고 있다.

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <div id="app"></div>

  <script>
    const $app = document.getElementById('app');
    
    const element1 = document.createElement('div');
    element1.textContent = 1;
    const element2 = document.createElement('div');
    element2.textContent = 2;
    
    $app.append(element1);

    let sum = 0;
    for (let i = 0; i < 2; i += 1) {
      sum += i;
    }

    Promise.resolve().then(() => $app.append(element2));
  </script>
</body>
</html>

 

위 코드의 결과를 퍼포먼스 탭에서 확인하면 다음과 같다.

스크립트가 호출되고 마이스코 태스크까지 모두 호출이 완료된 후 레이아웃(이미지 가장 오른쪽)이 호출되는 것을 확인할 수 있다. 마이크로 태스크 실행은 이미지 하단에 표시되어 있다.

 

 

🎃 강제 레이아웃

레이아웃이 실행 되는 두 번째 시점은 강제 레이아웃을 실행하도록 요인이 발생했을 때이다. 일반적으로 강제 레이아웃을 발생시키는 요인은 DOM을 읽는 연산이 발생할때이다.

 

대표적으로 offset으로 시작하는 속성을 호출하는 경우 강제 레이아웃을 발생시킨다.

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <div id="app"></div>

  <script>
    const count = 10;

    function createElement(i) {
      const element = document.createElement('div');
      element.textContent = `${i}번째 div`;
      return element;
    }

    function appendChild() {
      const $app = document.getElementById('app');

      const start = performance.now();
    
      for (let i = 0; i < count; i += 1) {
        const element = createElement(i);
        $app.appendChild(element);

        element.offsetHeight;
      }
    }

    appendChild();
  </script>
</body>
</html>

 

 

위의 코드 실행 결과를 퍼포먼스 탭에서 확인해보면 다음과 같다. 지금까지 보아왔던 결과랑 확연히 차이가 나는 것을 확인할 수 있다.

 

offsetHeight속성에 접근할 때보다 레이아웃이 발생하는 것을 확인할 수 있다.

 

 

강제 레이아웃을 발생키는 대표적인 요인들은 다음과 같다.

  • offset*
  • client*
  • scroll* / scrollBy() / scrollTo()
  • getBoundingClientRect()
  • focus()
  • innerText
    • innerHTML, textContent는 리플로우를 발생시키지 않는다.
  • window.scroll*
  • window.inner*
  • mouseEvent.layer* / mouseEvent.offset*

 

innerHTML, textContent와 달리 innerText만 강제 리플로우를 발생시키는 이유는, innerText는 요소 또는 하위 요소가 숨겨져 있는 경우는 무시하고 보이는 텍스트만을 처리하기 때문이다. textContent는 보이는지 여부와는 무관하게 텍스트만을 반환한다.

 

 

🎯 결론

자연스럽게 레이아웃이 발생하는 것이 아닌, 강제로 레이아웃을 발생시키는 요인들이 있다. 이를 너무 자주 활용하는 경우 빈번한 레이아웃을 발생시켜 성능을 나쁘게 만들 수 있다.