DOM Rendering Performance


Contents

  • 요소 추가 위한 For loop 사용
  • 코드 performance 테스트
  • Document Fragment 이용하기
  • Reflow & Reprint
  • Virtual DOM
  • Event Loop

요소 추가하기 위한 For loop 사용

지난 DOM 관련 포스팅에서 웹페이지에 요소를 추가하기 위해 아래와 같이 for loop을 사용했습니다.

지난 포스팅 링크

for(let i = 1; i <= 200; i++) {
  const newElement = document.createElement('p');
  newElement.textContent = 'This is paragraph number ' + i;

document.body.appendChild(newElement);
}

위의 코드는 paragraph 요소를 생성하고 텍스트를 요소에 추가합니다. 그리고 이 요소를 body 요소의 맨 마지막 자리에 추가를 합니다. 이 작업을 200번 반복합니다.

요소를 200개 추가를 해야하기 때문에 for loop를 사용해야 합니다. 하지만, 코드를 수정해서 좀 더 효율적인 코드를 작성할 수 있습니다. paragraph 요소를 감싸는 부모 요소를 만들고, 이 요소만 for loop가 끝난 후 body 요소에 추가를 하면됩니다.

const myCustomDiv = document.createElement('div');

for(let i = 1; i <= 200; i++) {
  const newElement = document.createElement('p');
  newElement.innerText = 'This is paragraph number ' + i;

  myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

코드 performance 테스트

위의 코드가 더 효율적인 코드인지 확인을 하는 방법 중 하나는 코드 실행 시간을 비교하는 것입니다. 코드의 실행 시간을 확인하기 위해서 우리는 performance.now( )를 사용할 수 있습니다.

performance.now는 1000분의 5 밀리초(5 마이크로초) 단위의 정밀한 시간 기록을 반환합니다. 아래와 같은 단계로 performance.now( )를 이용해 코드 속도를 확인할 수 있습니다.

  1. performance.now를 실행하고 변수에 할당함.
  2. 테스트 대상 코드를 실행함.
  3. 또다른 performance.now를 실행하고 변수에 할당함.
  4. 두번째 시간에서 첫번째 시간을 뺄셈을 해 시간을 확인함.
const startingTime = performance.now();

for (let i = 1; i <= 100; i++) {
  for (let j = 1; j <= 100; j++) {
    console.log(`i is ${i}, and j is ${j}.`);
  }
}

const endingTime = performance.now();
console.log('This code took ' + (endingTime - startingTime) + ' milliseconds.');

Document Fragment 이용하기

위에서 우리는 div 요소에 모든 paragraph를 담고 body 요소에 마지막에 한 번에 추가를 했습니다. 이와 같은 방식에서 한 가지 아쉬운 점은 꼭 필요하지 않은 'div' 요소를 생성한 것입니다.

새로운 요소를 페이지에 추가하면, 브라우저는 reflow(new screen layout을 결정)와 repaint 과정을 거칩니다. 그리고 이 과정은 시간이 소요됩니다.

우리는 요소를 추가하는 매 순간 브라우저에 바로 넣지 않고, 한 번에 몰아서 요소를 추가하고 싶습니다. 또한, 불필요한 요소(ex, 위 코드에서 'div'요소)를 생성하고 싶지도 않습니다. 자, 그러면 어떻게 해야 할까요? 우리에겐 documentFragment가 있습니다.

Document Fragment

document fragment는 부모 요소가 없는 경량화된 document 객체를 나타냅니다. document객체와 같이 DOM tree를 담을 수 있습니다. 하지만, 활성화된 document 객체가 아니기 때문에 reflow를 발생시키지않고, 성능에도 영향이 없습니다. document fragment를 사용해서 위의 코드를 아래와 같이 변경할 수 있습니다.

const fragment = document.createDocumentFragment();

for (let i = 0; i < 200; i++) {
  const newElement = document.createElement('p');
  newElement.innerText = 'This is paragraph number ' + i;

  fragment.appendChild(newElement);
}

document.body.appendChild(fragment);

Reflow & Reprint

Reflow는 브라우저가 웹페이지의 레이아웃을 처리하는 과정을 말합니다. 처음 DOM을 웹 상에 나타낼 때 발생하며, 화면 상 뭔가 변화가 있을 때마다 발생합니다. 이 과정은 꽤 expensive(slow)한 처리 과정입니다.

Repaint는 새로운 레이아웃을 브라우저에 그리는 과정입니다. 이 과정은 상대적으로 빠르지만, 과정이 많이 반복되지 않도록 해야합니다.

예를들면, 웹페이지 상 CSS class를 추가하면, 브라우저는 전체 레이아웃을 재계산합니다. 이 과정이 한번의 reflow와 한번의 repaint를 거친 과정입니다. 좀 더 구체적인 예는 아래와 같습니다.

<div id="comments">
  <div class="comment"><!-- some comments --></div>
  <div class="comment"><!-- some comments --></div>
  <div class="comment"><!-- some comments --></div>
</div>

위와 같은 html 요소가 있고, comment class를 가진 요소 중 두 개를 삭제해야 한다고 가정해 봅시다. 단순히 removeChild( ) 메서드로 두 요소를 삭제할 수 있습니다. 이 경우 2번의 reflow와 2번의 repaint가 발생합니다.

또 한가지 방법으로 document fragment를 생성해 comments id 요소 전체를 변경할 수도 있습니다. 이 경우 1번의 reflow와 1번의 repaint가 발생합니다.

또한, 우리는 comments 요소를 hide 한 뒤 화면상에 보이지 않게 한 상태에서 원하는 요소를 삭제하고 다시 화면에 출력을 할 수도 있습니다. 1번의 reflow와 1번의 repaint가 발생하기 때문에 상대적으로 속도가 빠릅니다.


Virtual DOM

위와 같은 이슈로 React와 같은 virtual DOM 라이브러리가 인기를 가지는 이유입니다. DOM에 직접적인 변경을 하지 않고 virtual DOM을 만든 후 라이브러리를 이용해 효율적으로 화면에 출력할 수 있기 때문입니다.


Event Loop

자바스크립트는 'run-to-completion', 'single-threaded'의 특성을 가진 언어입니다. 한번에 하나의 명령어를 순차적으로 처리합니다. 하지만, 자바스크립트에는 비동기로 처리되는 명령어가 있습니다. 코드가 바로 실행되는 것이 아닌, 이후에 실행이 되도록 할 수 있습니다. 아래의 event listener는 바로 실행이 되지 않고, 'keypress' 이벤트가 발생 시 실행이 됩니다.

const links = document.querySelectorAll('input');
const thirdField = links[2];

thirdField.addEventListener('keypress', function(event) {
  console.log('a key was pressed.');
})

그러면 어떻게 listener 함수가 비동기적으로 실행이 되는 것일까요? Call stack에서는 한번에 하나의 함수가 실행되고, 실행이 완료되면 해당 call stack이 사라지는데, 이벤트 리스너는 어떻게 실행이 되는 것일까요? 바로 Event Loop가 있어 가능합니다.

Event Loop 관련해서 아래의 세 단계에 대해서 살펴보겠습니다.

그리고 아래의 이벤트 리스너를 추가하는 코드를 살펴보겠습니다.

console.log('howdy');  // step-1

document.addEventListener('click', function() { // step-2
  console.log('123');
});

console.log('Ice cream tasty.'); // step-3

우리가 많이 사용하는 addEventListener와 setTimeout은 사실 자바스크립트 코드가 아니고, Web API입니다. 위의 코드를 브라우저에서 step 1, 2, 3로 순차적으로 실행 후, step-2의 이벤트 리스너를 'click'이벤트가 발생할 때까지 브라우저에서 간직하고 있습니다.

그리고 'click'이벤트가 발생하면, 해당 리스너는 Event queue로 전달됩니다.

'run-to-completion'은 Execution context에 현쟁 작동중인 context가 있다면, 해당 context가 끝날때까지 코드가 실행됩니다. 만약 call stack에 현재 실행 중인 context가 없고, Event Queue에 대기중인 리스너가 있다면 리스너가 실행이 됩니다.