React는 Virtual DOM과 재조정 알고리즘을 통해 자체적으로 렌더링 최적화를 수행한다. 하지만 웹 애플리케이션의 렌더링 성능은 CSS와 JavaScript 코드의 품질에 많은 부분을 의존한다. 따라서 올바른 CSS 선언과 JavaScript 메소드를 사용하여 Reflow와 Repaint를 최소화하는 것이 React의 내장된 최적화 기능을 더 효과적으로 활용하는 데 중요하다고 할 수 있겠다. 이 글에서는 브라우저의 렌더링 파이프라인 중 Reflow와 Repaint 를 렌더링 최적화의 관점에서 공부한 내용을 기록해두었다.
Reflow와 Repaint란?
브라우저의 렌더러 프로세스는 HTML과 CSS, Javascript를 각각 파싱하여 렌더링에 필요한 모든 정보를 담고 있는 Render tree를 계산한다. 이후 Reflow와 Repaint 과정을 거쳐 레스터화된 레이어들이 합성되어 화면에 표시되게된다.
Reflow(리플로우)
- Reflow는 웹 페이지의 레이아웃이 (다시) 계산되는 과정이다. 뷰포트 내에서 렌더 트리상 노드의 정확한 위치와 크기 즉 기하학적 정보를 계산하는 과정이다. 한 Element의 Reflow 가 발생하면 DOM tree상 모든 하위, 상위 요소의 후속 Reflow를 유발하기 때문에 비용이 큰 과정이다.
- DOM 노드를 추가, 제거 하는 경우
- DOM 요소의 위치 변경, 크기 변경을 하는 경우 (직접 조작 뿐 아니라 애니메이션 생성도 해당)
- display: none 으로 DOM 요소를 숨기거나 나타내는 경우
- 윈도우 리사이즈
- 글꼴, 글꼴 크기, 스타일을 변경하는 경우
- DOM 요소의 스타일을 직접 수정하는경우(아래 CSS 속성 참고)
- 스타일 시트가 추가되거나 제거되는 경우
JS, CSS 파싱 → DOM/CSSOM 변경 → Render tree 계산 → Reflow → Repaint → Layer update → Composite
대표적인 Reflow 발생 원인은 아래와 같다.
대표적인 Reflow의 발생 원인
Reflow를 발생시키는 CSS 속성
position
width
height
left
top
right
bottom
margin
padding
border
border-width
display
float
font-size
font-failmy
line-height
overflow
Repaint(리페인트)
- Repaint는 각 노드의 paint 메소드가 호출되며 발생한다. Element에서 레이아웃에는 영향을 미치지 않는 시각적 스타일에 변경이 생길 때 발생한다. 배경색, 텍스트 색상, 테두리 등의 시각적 속성이 변경되면 해당 요소만을 다시 그리게 된다. Layout tree 를 다시 계산하지는 않음으로 Reflow에 비해 비용이 적다.
Repaint → Layer update→ Composite
Repaint를 발생시키는 CSS 속성
background
background-image
background-size
border-radius
border-size
color
inlin-style outline-color
outline-style
outline-width
text-decoration
하드웨어가속(GPU 단에서 렌더링)
- 특정한 CSS 속성들은 전체 DOM의 Reflow와 Repaint가 발생하지 않고, 속성이 사용된 특정 레이어들만 분리되어 GPU 계산 후 합성된다.
Layer update→ Composite
레이어를 분리하여 Reflow & Repaint를 모두 발생시키지 않는 CSS 속성
transform
opacity
Reflow & Repaint 최소화 - 브라우저 렌더링 최적화 하기
- DOM 요소의 개별 스타일 직접 수정을 피하기
- 스타일이 변경되어있는 클래스 이름으로 변경
- cssText 를 직접 수정
대신
//👍 element.className = 'newClassName'; element.classList.remove('oldClassName'); element.classList.add('newClassName'); element.style.cssText = 'color: blue; font-size: 20px;'; //👎 element.style.color = 'blue'; element.style.fontSize = '20px';
- DOM 변경사항을 한꺼번에 일괄처리
- 반복문 등에서 변경점을 모아서 한번에 처리하는 방법,
- display: none 으로 변경 후 요소에 변경을 가하고 이후 다시 display를 노출되게끔 변경하는 방법
- DOM 요소를 복제한 후, 변경을 가하고 치환하는 방법
등이 이에 포함된다.
아래 코드에서 버튼을 누르면 반복문을 도는 3000번의 횟수만큼 DOM이 연산된다. 때문에 DOM이 한번만 연산될 수 있도록 코드를 수정해야한다.
//👍 <script> function onClick() { const $ul = document.getElementById('ul'); let list = ""; for (let i = 0; i< 3000; I ++) { list += `<li>${i}<li>`; } $ul.innerHTML = list; } </script> //👎 <script> function onClick() { const $ul = document.getElementById('ul'); for (let i = 0; i< 3000; I ++) { $ul.innerHTML += `<li>${i}<li>`; } } </script> ... <body> <button onclick='onClick()'> 리스트 추가하기 </button> <ul id='ul'></ul> </body>
- 한번 요청된 스타일 정보를 변수에 캐싱하기
offset, scrollTop과 같은 계산된 스타일 정보를 요청할 때마다 정확한 정보를 제공하기 위해 큐를 비우고 모든 변경사항을 적용하기 때문에 스타일 정보를 변수에 저장하여 사용하는 것이 권장된다.
let top = el.offsetTop, elStyle = el.style; //👍 for (let i = 0; i ‹ len; i++) { top += 10; //스타일 정보 변수에 저장하여 사용 elStyle. top = '${top }px*; } //👎 for (let i = 0; i ‹ len; i++) { el.style.top = ${ el.offsetTop + 10 }px*; }
- Reflow가 발생하는 Element의 Layer 분리하기
최신 브라우저는 Element의 Layer 를 분리하여 렌더링 과정을 별도로 수행하기도 한다.
레이어 분리 기준
1. Position: fixed or absolute 이 사용된 경우 → 주로 이 방법을 사용
2. video, canvas, iframe 태그가 사용된 경우
3. Transform 3D 관련 속성이 사용된 경우
4. will-change 등의 속성이 사용된 경우
…
- 가능하다면
transform
opacity
를 대신하여 사용하기
- <table> 태그 사용 피하기
- IE의 CSS 표현식 사용 피하기
- 타이머 (setTimeout, setInterval) 사용 시 되도록 실행 사이클 안에서 실행하도록 처리