내가 자주 헷갈리는 개념인 reflow와 repaint에 대해서 공부한 내용을 정리해 보려고 한다.
브라우저 렌더링
브라우저의 주요 기능은 사용자가 선택한 자원을 서버에 요청하고 브라우저에 표시하는 것이다. 여기서 자원은 일반적으로 HTML 문서이지만, 다른 형태의 파일(e. g. 이미지, PDF 등)일 수도 있다. 브라우저는 HTML과 CSS 명세에 따라 HTML 파일을 해석하여 표시하는데 이 명세는 웹 표준화 기구인 W3C에서 정한다.
브라우저의 주요 구성 요소는 다음과 같다.
- 사용자 인터페이스 : 요청한 페이지를 보여주는 창 제외한 나머지 모든 부분 (e.g. 북마크, 주소 표시줄 등)
- 브라우저 엔진 : 사용자 인터페이스와 렌더링 엔진 사이의 동작 제어
- 렌더링 엔진 : 요청한 콘텐츠 표시. 예를 들어 HTML을 요청하면 HTML과 CSS를 파싱하여 화면에 표시함
- 통신 : HTTP 요청과 같은 네트워크 호출에 사용됨.
- UI 백엔드 : 콤보 박스와 창 같은 기본적인 장치를 그림.
- 자바스크립트 해석기 : 자바스크립트 코드를 해석하고 실행
- 자료 저장소 : 자료를 저장하는 계층. 쿠키를 저장하는 것과 같이 모든 종류의 자원을 하드 디스크에 저장할 필요가 있다. HTML5 명세에는 브라우저가 지원하는 ‘웹 데이터베이스'가 정의되어 있다.
렌더링 엔진의 역할은 요청 받은 내용을 브라우저 화면에 표시하는 일이다. 렌더링 엔진의 동작과정을 살펴보면, 렌더링 엔진은 통신으로부터 요청한 문서의 내용을 얻는 것으로 시작하는데 문서의 내용은 보통 8KB 단위로 전송된다.
다음은 렌더링 엔진의 기본적인 동작 과정이다.
렌더링 엔진은 HTML 문서를 파싱하고 “콘텐츠 트리" 내부에서 태그를 DOM 노드로 변환한다. 그 다음 외부 CSS 파일과 함께 포함된 스타일 요소도 파싱한다. 스타일 정보와 HTML 표시 규칙은 “렌더 트리”라고 부르는 또 다른 트리를 생성한다.
렌더 트리 구축은 색상 또는 면적과 같은 시각적 속성이 있는 사각형을 포함하고 있는데 정해진 순서대로 화면에 표시된다.
렌더 트리 생성이 끝나면 배치가 시작된다. 이 작업은 각 노드가 화면의 정확한 위치에 표시되는 것을 의미한다. 렌더링 엔진은 좀 더 나은 사용자 경험을 위해 가능하면 빠르게 내용을 표시하는데 모든 HTML을 파싱할 때까지 기다리지 않고 배치와 그리기 과정을 시작한다. 네트워크로부터 나머지 내용이 전송되기를 기다리는 동시에 받은 내용의 일부를 먼저 화면에 표시하는 것이다.
게코는 시각적으로 처리되는 렌더 트리를 형상 트리(frame tree)
라고 부르고 각 요소를 형상(frame)이라고 하는데 웹킷은 렌더 객체(render object)
로 구성되어 있는 렌더 트리(render tree)
라는 용어를 사용한다.
Reflow
Reflow는 렌더링 엔진에서 요소를 배치하는 과정을 의미한다. 위에서 본 렌더 트리 구축
단계에서 DOM 트리와 스타일 규칙을 합쳐서 렌더 트리를 만들고, 여기에서 reflow를 통해 각각의 요소들의 레이아웃을 위치시킨다.
여기서 렌더 트리는 DOM 요소를 기반으로 만들어지지만, 완전히 대응되지는 않는다. DOM 트리가 문서의 구조를 나타낸다면 렌더 트리는 문서의 시각적 구조를 나타낸다. 예를 들어 스타일에 display: none 속성이 있다면 DOM에는 존재하지만 시각적으로는 없기에 렌더 트리에는 할당되지 않는다.
reflow가 발생하는 경우는 다음과 같다. (여기에 적은 케이스는 예시일 뿐, 이 외에도 reflow가 발생할 수 있는 경우는 물론 더 있다)
- DOM 노드의 추가, 제거
- DOM 노드의 위치 변경
- DOM 노드의 크기 변경 (margin, padding, border, width, height 등)
- CSS3 애니메이션과 트랜지션
- 폰트 변경, 텍스트 내용 변경
- 이미지 크기 변경
- offset, scrollTop, scrollLeft와 같은 계산된 스타일 정보 요청
- 페이지 초기 렌더링
- 윈도우 리사이징
Repaint
Repaint는 렌더 트리가 탐색되고 paint 메서드가 호출되어서 UI 기반의 구성요소를 사용해서 그리는 과정이다. repaint가 이루어지기 위해서는 렌더트리가 있어야 하고 따라서 Reflow 작업이 이루어진 후에 repaint 작업이 이루어지는 것을 알 수 있다. 화면의 구조가 변경이 될 때는 reflow와 repaint가 모두 발생한다.
다만 repaint가 발생하기 위해서 항상 reflow가 발생해야 하는 것은 아니다. reflow가 발생하지 않고 repaint만 발생하는 경우도 있는데, 예를 들면 레이아웃에 영향을 주지 않는 엘리먼트 개별의 변화에 대해서는 repaint만 발생한다. color, background-color, visibility 같은 속성이 그러한 경우이다.
Reflow 최적화
reflow는 비용을 발생시키는 절차이므로 가능한 안 하는 것이 성능 측면에서 유리하다. 그렇다면 어떻게 하면 reflow를 줄일 수 있을까?
1. 스타일을 변경할 경우 가장 하위 노드의 클래스를 변경한다.
DOM 노드의 크기 또는 위치가 변경되면 하위 노드와 상위 노드에도 영향을 미칠 수 있다. 이 때 가장 하위 노드의 스타일을 변경할 경우, 전체 노드가 아닌 일부 노드로 reflow를 영향을 최소화 할 수 있다.
2. 인라인 스타일을 사용하지 않는다.
인라인 스타일은 HTML이 파싱될 때, 레이아웃에 영향을 미쳐 추가 리플로우를 발생시킨다. 또한 관심사 분리가 제대로 이루어지지 않으면 유지 보수가 힘들어 진다.
3. 애니메이션이 있는 노드는 position을 fixed 또는 absolute로 지정한다.
애니메이션 효과는 많은 reflow 비용을 발생시킨다. position 속성을 fixed 또는 absolute의 값으로 주어, 지정된 노드를 전체 노드에서 분리시켜 해당 노드에서만 reflow가 발생하도록 제한시킬 수 있다.
애니메이션 효과를 줘야 하는 노드에 position 속성이 적용이 되지 않았다면 애니메이션 시작 시 position 속성 값을 fixed 또는 absolute로 변경하였다가 애니메이션 종료 후 다시 원복시켜서 렌더링을 최적화 할 수 있다.
4. table 레이아웃을 피한다.
<table>
은 점진적으로 렌더링 되지 않고, 모두 로드되고 테이블 너비가 계산된 후에 화면에 그려진다. 테이블 안의 컨텐츠의 값에 따라 테이블 너비가 계산 된다. 따라서 테이블 컨텐츠의 작은 변경만 있어도 테이블 너비가 다시 계산되고 테이블의 모든 노드들이 reflow가 발생한다. 이러한 이유로 <table>
을 레이아웃 용도로 사용하는 일은 피해야 한다.
5. CSS 하위 선택자를 최소화 한다.
CSS 하위 선택자를 최소화 하는 것은 reflow 횟수를 줄이는 방법이 아니라 렌더 트리 계산을 최소화 하는 방법에 대한 내용이다.
<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 하위 선택자를 최소화 하는 것이 렌더링 성능에 더 좋다.
렌더 트리는 DOM과 CSSOM이 합쳐져서 만들어진다. DOM은 HTML이 파싱되어 만들어진 트리이고, CSSOM은 CSS가 파싱되어 만들어진 트리이다. 두 트리를 결합하여 렌더 트리를 만드는데, CSS 하위 선택자가 많아지면 CSSOM 트리의 깊이(Depth)가 깊어지게 되고 결국 렌더 트리를 만드는 시간이 더 오래 걸리게 된다.
6. 숨겨진 노드의 스타일을 변경한다.
display: none 으로 숨겨진 노드를 변경할 때는 reflow가 발생하지 않는다. 숨겨진 노드를 표시하기 전에 노드의 컨텐츠를 먼저 변경한 후 화면에 나타내면 reflow를 줄일 수 있다.
7. DOM 사용을 최소화한다.
reflow 비용을 줄이기 위해서 DOM 노드 사용을 최소화 해야 한다. 한 가지 방법은 DOM Fragment를 사용하여 DOM을 추가할 때 마다 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);
위의 코드와 같이 createDocumentFragment 를 사용하여 한 번에 DOM 을 추가하여 DOM 접근을 최소화 할 수 있다.
8. 캐시를 활용한다.
브라우저는 레이아웃 변경을 큐에 저장했다가 한 번에 실행하여 reflow를 최소화 한다. 하지만 offset, scrollTop 과 같은 계산된 스타일 정보를 요청할 때마다 정확한 정보를 제공하기 위해 큐를 비우고 모든 변경사항을 적용한다.
// Bad practice
for (let i = 0; i < len; i++) {
el.style.top = `${ el.offsetTop + 10 }px`;
el.style.left = `${ el.offsetLeft + 10 }px`;
}
// Good practice
let top = el.offsetTop, left = el.offsetLeft, elStyle = el.style;
for (let i = 0; i < len; i++) {
top += 10;
left += 10;
elStyle.top = `${ top }px`;
elStyle.left = `${ left }px`;
}
이런 낭비를 해결하기 위해 위의 코드와 같이 스타일 정보를 변수에 저장하여 offset, scrollTop 등의 값 요청을 최소화해야 한다.
참고자료
https://d2.naver.com/helloworld/59361
https://velog.io/@young_pallete/Reflow-Repaint%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
'Web Frontend Developer' 카테고리의 다른 글
FEConf 2022 참석 후기 및 세션 요약 (0) | 2022.10.19 |
---|---|
[Three.js] Transform Objects (1) | 2022.10.17 |
[웹 프론트엔드 인터뷰] #4. useCallback과 useMemo는 언제 어떻게 사용하나요? (0) | 2022.07.20 |
React Context 올바르게 사용하기 (0) | 2022.07.06 |
Apollo Client에서의 Caching에 대하여 (0) | 2022.06.14 |