브라우저 렌더링은 렌더 트리의 생성 이후 다음 두 단계를 거친다.
reflow는 Layout 단계에서 수정이 있을 때,
repaint는 Paint 단계에서 수정이 있을 때 발생한다.
reflow는 repaint의 상위 과정이기 때문에 reflow가 발생하면 대부분 repaint도 자연히 발생한다.
렌더 트리와 DOM의 차이
렌더 트리는 DOM을 기반으로 만들어진다.
DOM 트리는 전체 문서의 구조를 나타내고, 렌더 트리는 시각적인 구조를 나타낸다.
예를 들어,display: none
속성을 가진 요소는 DOM 트리에는 나타나지만 렌더 트리에는 시각적으로 없는 요소이기 때문에 나타나지 않는다.
DOM 트리의 순서와 상관없이 시각적으로 별개인 요소(float, position 등)도 DOM 트리와는 다른 위치의 렌더 트리 노드에 할당된다.
Layout 단계에서 수정이 발생하여 각 노드의 위치를 렌더 트리에 다시 렌더링할 때 발생한다.
우선 최초에 한 번 렌더링이 되고, 이후 요소들의 레이아웃에 영향을 주는 변화가 생기면 다시 렌더 트리를 구성하게 된다.
reflow 횟수가 많아지면 성능이 저하된다.
다음과 같은 방법들이 권장된다.
// Bad
const body = document.body;
body.style.width = '50px';
body.style.height = '100px';
// style.width와 style.height는 reflow 추가 발생
// Good
const body = document.body;
body.style.cssText = 'width: 50px; heigh: 100px;'; // cssText로 대체
// bad
const ulElement = document.getElementsByTagName('ul')[0];
for (let i = 0; i < 10; i++) {
ulElement.innerHTML += `<li>list${i}</li>`;
}
// good
const ulElement = document.getElementsByTagName('ul')[0];
let strHtml = ulElement.innerHTML; // 가상의 상위 요소 추가
for (let i = 0; i < 10; i++) {
strHtml += `<li>list${i}</li>`; // 가상의 상위 요소에 동적으로 요소 추가
}
ulElement.innerHTML = strHtml; // DOM에 가상의 상위 요소를 추가
const frag = document.createDocumentFragment();
const ul = frag.appendChild(document.createElement('ul'));
for (let i = 1; i <= 3; i++) {
li = ul.appendChild(document.createElement('li'));
li.textContent = `item ${ i }`;
}
document.body.appendChild(frag);
2번과 비슷한 방법이다.
document.createDocumentFragment
를 사용하여 Fragment를 만든 뒤, 모든 조작을 그 안에서 하고 마지막에는 Fragment만 실제 DOM에 추가한다.
<div class="reflow_box">
<ul class="reflow_list">
<li>
<button type="button" class="btn">버튼</button>
<li>
<li>
<button type="button" class="btn">버튼</button>
<li>
</ul>
</div>
/* 잘못된 예 */
.reflow_box .reflow_list li .btn{
display:block;
}
/* 올바른 예 */
.reflow_list .btn {
display:block;
}
CSS 하위 선택자가 많아지면 CSSOM 트리의 depth가 깊어져서 렌더 트리를 만드는 시간이 더 오래 걸리게 된다.
브라우저는 Layout 변경을 큐에 저장했다가 한 번에 실행하여 reflow를 최소화한다. 하지만 offset, scrollTop과 같은 계산된 스타일 정보는 정확한 정보를 제공하기 위해 매번 큐를 비우고 모든 변경 사항을 적용하므로 reflow가 여러 번 일어난다.
// Bad
for (let i = 0; i < len; i++) {
el.style.top = `${ el.offsetTop + 10 }px`;
el.style.left = `${ el.offsetLeft + 10 }px`;
}
// Good
let top = el.offsetTop; // 계산되는 style 값을 먼저 변수에 저장
let left = el.offsetLeft; // 계산되는 style 값을 먼저 변수에 저장
let elStyle = el.style; // 공통되는 부분을 변수에 저장하여 연산 최소화
for (let i = 0; i < len; i++) {
top += 10; // 값을 더하고
left += 10;
elStyle.top = `${ top }px`; // style에 추가하는 것을 반복
elStyle.left = `${ left }px`;
}
위의 코드와 같이 스타일 정보를 변수에 저장하고 한 번에 처리하는 방식으로 offset, scrollTop 등의 값 요청을 최소화할 수 있다.
애니메이션 효과는 reflow 비용이 크다. 애니메이션 노드에 position: fixed나 absolute 속성을 주면 해당 노드를 전체 노드에서 분리시켜서 그 노드에서만 reflow가 발생하게 할 수 있다.
애니메이션이 시작할 때 position 값을 fixed 또는 absolute로 변경, 애니메이션이 종료된 후 다시 원상 복구시킨다.
상위 노드의 크기 또는 위치를 변경하면 하위 노드에까지 영향
=> 가장 하위 노드의 스타일을 변경하면 reflow를 최소화할 수 있다.
HTML이 파싱될 때 Layout에 영향을 주어서 추가적인 reflow를 발생시킨다.
table
을 활용한 Layout 피하기<table>
은 데이터가 모두 load되고 나서 + 테이블 너비가 계산된 후에 화면에 그려진다. (테이블 안의 컨텐츠 값에 따라 테이블 너비가 계산되기 때문)
따라서 테이블 컨텐츠의 작은 변경만 있어도 테이블 너비가 다시 계산되고, 테이블의 모든 노드에 reflow가 발생한다. <table>
을 Layout 용도로 사용하는 일은 피하자.
display: none
활용 (숨겨진 노드의 css 변경)display: none
인 노드를 변경할 때는 reflow가 발생하지 않는 점을 이용한다.
repaint는 reflow의 하위 과정이므로 대부분 reflow가 발생하면 repaint가 자연히 발생한다.
하지만 reflow는 layout이나 position에 대한 변화가 생겨서 일부 혹은 전체를 다시 구성하고 그리는 반면, repaint는 layout이나 position에 영향이 없는 개별적인 변화(예: color, background-color, visibility 등)가 발생할 때 일어난다.
즉, reflow는 레이아웃(배치), 요소의 위치, 크기 변경 시 발생하고, repaint는 단순히 요소의 일부만 변경되었을 때 다시 그리는 것이다.
브라우저의 이해 #1 Reflow, Repaint에 대하여 알아봅니다.
Reflow와 Repaint에 대하여