렌더링

렌더링 과정

렌더링이란 논리적인 문서(DOM)의 표현식을 그래픽 표현식으로 변형시키는 과정이다. 이 과정은 다음과 같이 크게 2단계를 거쳐 이뤄진다.

  1. DOM 요소와 스타일에 기반을 둔 레이아웃 계산
  2. 계산된 요소의 화면 표현

일반적인 전체 흐름은 브라우저에 문서가 로딩됨에 따라 DOM 트리의 구성이 진행되면 레이아웃을 계산한 후 문서에 요소를 그린다.

렌더링이 진행되는 과정

DOM 트리 생성

브라우저는 HTML 태그를 파싱해 DOM 트리를 구성한다. DOM은 데이터의 표현식으로 모든 HTML 태그에는 그에 상응하는 노드가 있으며, 태그 사이에는 텍스트 데이터가 포함될 수 있는데 이 또한 텍스트 노드의 표현식이다.

각 태그는 태그 데이터의 표현식인 DOM 요소로 1:1로 대응해 표현되며, DOM 요소 노드는 트리 형태로 구성된다. 이를 DOM 트리라 한다.

스타일 구조체 생성

스타일 정보를 통해 스타일 구조체를 생성한다. 스타일 정보는 단계적으로 처리되며, 가장 마지막 단계의 스타일 정보가 이전 스타일보다 우선으로 적용된다. 스타일 정보는 다음과 같이 3단계로 나누어 처리된다.

  1. 브라우저 자체에 포함된 기본 스타일 정보
  2. 사용자 정의 스타일
  3. HTML 인라인 스타일

렌더 트리 생성

DOM 트리와 스타일 구조를 통해 렌더 트리를 생성한다.
렌더 트리는 DOM 트리와는 다르게 각 노드에 스타일 정보가 설정되어 있고 화면에 표현되는 노드로 구성된다. 어떤 노드의 스타일이 ‘display: none’ 으로 설정되어 있으면 해당 노드는 렌더 트리에 포함되지 않는다. 그렇기 때문에 DOM 트리와 렌더 트리의 노드는 서로 1:1로 대응되지 않는다. head, title, script 등과 같이 화면에 표현되는 노드가 아니므로 DOM 트리에는 포함되어 있지만 렌더 트리에는 포함되어 있지 않다.

레이아웃 처리

렌더 트리의 각 노드의 크기가 계산되고 문서에서 정확한 위치에 배치되도록 위치를 계산한다. 루트에서 하위 노드로 반복되며 진행한다.

페인트

렌더 트리를 순회하면서 페인트 함수를 호출해 노드를 화면에 표현한다.


리플로우(reflow) 와 리페인트(repaint)

렌더링이 모두 완료된 상태에서 사용자의 인터랙션 또는 기능에 따라 화면의 일부 영역에 변경 요인이 발생한다. 이러한 작업이 발생하면 구성돼 있는 렌더 트리가 변경되어야 하며 리플로우 또는 리페인트가 발생한다.

리플로우

변경이 필요한 렌더 트리에 대한 유효성 확인 작업과 함께 노드의 크기와 위치를 다시 계산한다. 노드의 ‘크기’ 또는 ‘위치’가 바뀌어 현재 레이아웃에 영향을 미쳐 배치를 다시 해야 할 때 리플로우가 발생한다.

특정 요소에 리플로우가 발생하면 요소의 DOM 구조에 따라 자식 요소와 부모 요소 역시 다시 계산될 수 있으며, 경우에 따라서는 문서 전체에 리플로우가 발생할 수도 있다.

리페인트

변경 영역의 결과를 표현하기 위해 화면이 업데이트 되는것을 의미한다. 리플로우각 발생하거나 배경색 변경 등의 단순한 스타일 변경과 같은 작업이 발생하는 경우다.

리플로우와 리페인트 모두 처리 비용이 발생하지만 리페인트보다 리플로우의 비용이 훨씬 높다. 리플로우는 변경 범위에 따라 전체 페이지의 레이아웃을 변경해야 할 수도 있기 때문이다. 어느 경우든 리플로우와 리페인트는 코드를 작성할 때 최소화해야 한다.

발생 요인

  • DOM 노드의 변경
  • DOM 노드의 노출 속성을 통한 변경: display: none 은 리플로우와 리페인트를 발생시키지만 비슷한 속성인 visibility: hidden 은 요소가 차지한 영역을 유지해 레이아웃에 영향을 주지 않으므로 리페인트만 발생한다.
  • 스크립트 애니메이션: 애니메이션은 DOM 노드의 이동과 스타일 변경이 짧은 시간 내에 수차례 반복해 발생되는 작업이다.
  • 스타일
  • 사용자의 액션

리플로우 최소화 방법

작업 그룹핑

DOM 요소의 정보를 요청하고 변경하는 코드는 같은 형태의 작업끼리 그룹으로 묶어 실행시키는 것이 좋다.

1
2
3
4
5
6
function change() {
var width = document.getElementById("layer1").style.width;
document.getElementById("layer2").style.width = width;
var height = document.getElementById("layer3").style.height;
document.getElementById("layer4").style.height = height;
}

위의 코드는 요소의 스타일 정보를 요청하고, 반환된 값을 다른 요소의 스타일 변경하는 데 사용한다. 그 후 다시 다른 요소에 동일한 형태의 작업이 반복된다. 이 코드를 실행하면 리플로우가 여러 번 발생할 수 있다.
스타일 조회 -> 변경 -> 리플로우 -> 스타일 조회 -> 변경 -> 리플로우

1
2
3
4
5
6
function change() {
var width = document.getElementById("layer1").style.width;
var height = document.getElementById("layer3").style.height;
document.getElementById("layer2").style.width = width;
document.getElementById("layer4").style.height = height;
}

위와 같이 비슷한 형태의 작업 끼리 그룹으로 묶어 실행되도록 순서를 변경하면 렌더링 처리를 향상시킬 수 있다.

스타일 조회 -> 스타일 조회 -> 변경 -> 변경 -> 리플로우

실행 사이클

브라우저에서 자바스크립트 실행은 이벤트 루프 모델을 따른다. 기본적으로 브라우저는 이벤트가 발생하면 바로 처리가 가능하도록 유휴(idle) 상태에 머무른다. 그러다 어떤 요청에 의해 유휴 상태가 해제되면 작업이 실행된다. 작업이 실행되면 브라우저는 작업의 실행 결과에 따른 리페인트가 완료될 때까지 기다린다. 이러한 실행 사이클로 인해 타이머를 사용하면 수차례의 리플로우와 리페인트가 발생될 수 있다.

1
2
3
4
5
6
function reflow() {
document.getElementById("box1").style.height = "50px";
setTimeout(function() {
document.getElementById("box2").style.height = "70px";
}, 0)
}

타이머의 설정 시간을 0으로 설정해도 브라우저가 유휴 상태(stack 이 비워져 있는 상태)가 아니면 그 상태가 되기까지 실행되지 않는다. 첫 번째 요소에 대한 작업이 한 사이클 내에서 실행되고, 타이머의 실행은 먼저 실행된 사이클이 끝난 다음에 진행된다. 이로 인해 결과적으로는 리플로우와 리페인트가 두 번 발생하게 된다. 이와 같이 리플로우와 리페인트가 일어날 수 있는 작업은 가능하면 한 실행 사이클 안에서 실행하도록 처리하는 편이 효과적이다.

노출 제어를 통한 리플로우 최소화 방법

요소의 스타일을 변경하면 리페인트는 반드시 일어나며, 변경 형태에 따라 리플로우도 일어난다.

display

기본적으로 리플로우와 리페인트는 모두 화면에 변경된 사항이 반영되는 시점에 발생한다. 여러 속성의 스타일을 변경하는 중간 단계에서는 화면에 표시하지 않고, 작업이 완료되고 최종 결과가 반영되는 마지막 시점에 요소를 다시 표시한다면 리플로우와 리페인트의 발생 횟수를 크게 줄일 수 있다.

이 코드는 값을 여러번 변경하며 값이 변경될 때마다 리플로우와 리페인트가 발생한다.

1
2
3
4
5
6
7
8
9
var element = document.getElementById("box1");

for (var i=50; i < 100; i++) {
element.style.width = i + "px"
}

for (var i=1; i < 50; i++) {
element.style.borderWidth = i + "px"
}

하지만 다음과 같이 요소를 보이지 않게 하고 모든 변경이 반영된 이후에 표시하면 처음과 마지막 시점 두 번으로 리플로우 발생 횟수가 줄어든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var element = document.getElementById("box1");

element.style.display = "none";

for (var i=50; i < 100; i++) {
element.style.width = i + "px"
}

for (var i=1; i < 50; i++) {
element.style.borderWidth = i + "px"
}

element.style.display = "block";

display 의 none, block 을 이용한 최소화 방법이다.

노드 복제

변경하려는 요소의 노드를 복제한 후 복제된 노드에 필요한 작업을 실행하는 방법이다. 복제된 노드는 DOM 트리에 추가된 상태가 아니므로 렌더링 성능에 영향을 줄 수 있는 작업을 실행하더라도 리플로우나 리페인트가 발생하지 않는다.

1
2
3
4
5
6
7
8
var element = document.getElementById("box1");
var clone = element.cloneNode(true);

for (var i=0; i< 100; i++) {
clone.style.width = i + "px";
}

parentNode.replaceChild(clone, element);

작업이 모두 완료된 이후 복제된 노드를 원래 노드와 치환해 DOM 트리에 변경된 사항이 적용되게 한다. 그러면 치환 시점에만 리플로우와 리페인트가 발생하는 것이므로 display 속성을 사용하는것 보다 적게 발생한다.

캐싱

여기서의 캐싱은 별도의 변수에 자주 사용하는 값을 저장하는 것이다. 특정 속성과 메서드를 사용하기만 해도 리플로우 발생하는 경우가 있다. 자주 사용하는 속성의 값이나 메서드의 반환값을 변수에 저장하면 직접 속성이나 메서드를 호출하는 횟수를 줄여 성능을 향상 시킬 수 있다.

1
2
3
4
for (condition) {
el.style.width = el.scrollWidth + "px";
el.style.height = el.scrollHeight + "px";
}

scrollWidth 와 scrollHeight 메서드는 호출하기만 해도 리플로우가 발생한다. 이 경우 값을 최대한 별도의 변수에 캐싱해 자주 호출되지 않게 하면 리플로우의 발생 빈도를 낮출 수 있다.

1
2
3
4
5
6
7
var scrollWidth = el.scrollWidth;
var scrollHeight = el.scrollHeight;

for (condition) {
el.style.width = scrollWidth + "px";
el.style.height = scrollHeight + "px";
}

하드웨어 가속 렌더링

브라우저는 웹 페이지 컨텐츠 렌더링 작업의 대부분을 CPU 에 의존해 왔다. 하지만 모바일 기기에도 GPU 가 기본으로 포함되고, 비디오, 3D 그래픽 등과 같이 화려하고 용량이 큰 컨텐트의 소비가 늘어 이를 활용하는 방법에 대한 고민도 커졌다.

GPU를 렌더링에 활용하면 성능에 이점이 있다. 일반적인 렌더링은 CPU 에서 렌더링 요소에 대한 연산 작업을 처리하면 그 결과값을 사용해 GPU 가 출력하는 과정을 거친다. 그러나 CPU 에서 처리되던 작업이 GPU 에 위임되면 처리 결과값을 GPU 로 전달하는 과정이 생략될 수 있고 CPU 도 다른 작업에 더 집중할 수 있다.

GPU 의 기본적인 하드웨어 디자인은 대용량의 픽셀 데이터를 조합하고 그리는 작업을 하게 되어 있으므로 CPU 보다 GPU 가 렌더링 작업을 최적으로 실행할 수 있다.

참조: 자바스크립트 성능 이야기

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.