렌더링 절차
아래 포스팅에서 렌더링 절차를 정리한 적 있다. 참고 바람
https://sysy1127.tistory.com/16
브라우저 렌더링 작동 원리: 파싱부터 렌더링까지
중요한 렌더링 경로 다섯 단계DOM 트리 구축 - CSSOM 구축 - 렌더 트리 생성 - 레이아웃 계산 - 페인트구문 분석(Parsing) - 프로그래밍 언어의 문법에 맞게 작성된 텍스트 문서를 읽고 실행하기 위해
sysy1127.tistory.com
1) 렌더 트리 생성
DOM과 CSSOM이 생성되면 이 둘은 결합하여 Render Tree를 생성한다.
렌더 트리는 CSS 스타일이 적용된 DOM 트리라고 생각하면 된다.
렌더 트리는 모든 HTML 요소가 포함된 DOM 트리와 달리, 브라우저 화면에 렌더링되는 노드만으로 구성된다.
따라서 불필요한 레이아웃 계산을 줄이고 렌더링 성능을 최적화한다.
HTML meta 태그, CSS의 display: none 처럼 브라우저 화면에 보여지지 않는 것들은 포함하지 않는다.
하지만 visible: hidden은 보이지는 않지만 공간을 차지하기 때문에 포함한다.
2) 레이아웃(Layout) 계산
- 렌더 트리를 기반으로 각 요소의 크기와 위치를 계산
- 브라우저 창 크기, 글꼴 크기, 뷰포트 크기 등을 고려하여 픽셀 단위의 정확한 위치를 결정.
- 예를 들어, width: 50% 가 설정된 요소는 부모 요소의 크기를 계산한 후 배치됨.
3) 페인트(Paint)
- 레이아웃이 완료되면 각 요소의 색상, 그림자, 텍스트 등의 시각적 스타일을 픽셀로 변환.
- 브라우저는 CPU를 사용하여 픽셀 데이터를 생성(→ GPU에 전달).
- 이미지, 배경색, 텍스트 색상 등 시각적 요소를 그리는 과정.
4) 합성(Composition)
- 일부 요소는 자신만의 독립적인 레이어(layer) 를 가질 수 있음.
- position: fixed;, transform: translate(), will-change, opacity 등이 적용된 요소는 GPU 레이어로 분리됨.
- GPU가 독립적인 레이어를 합성(Composite)하여 최종 화면을 출력.
- 이 단계에서 최적화가 가능!
- 예: transform: translateZ(0); 을 사용하면 GPU 가속을 유도하여 렌더링 속도를 높일 수 있음.
Reflow와 Repaint
리플로우(Reflow)와 리페인트(Repaint)는 요소가 시각적으로 변경되었을때 변화를 계산하여 화면에 그려주는 작업이다.
리플로우는 Layout 단계에, 리페인트는 Paint 단계에 발생한다.
리플로우는 리페인트의 상위단계로, 리플로우가 일어나면 대부분 리페인트도 함께 발생한다.
Reflow
생성된 DOM 노드의 레이아웃 수치가 변경 시 영향받는 모든 노드의 수치를 다시 계산하여 렌더트리를 재 생성 하는 과정
발생시점
- DOM 노드의 추가/제거
- DOM 노드의 위치/크기 변경 (width, margin, border 등)
- Window 창 크기 조절 (위치/크기 값이 상대 값일 경우)
- CSS 애니메이션
- 계산된 css 정보 (offset, scrollTop 등)에 접근할 때
- 폰트 변경
- 이미지 크기 변경
- 텍스트 변경
리플로우는 비용이 큰 작업인데, 특정 요소에서 리플로우가 발생하면 부모나 형제같은 주변 요소들도 영향을 받기 때문이다.

Repaint
변경된 요소를 화면에 그려주는 작업
발생시점
- 리플로우가 발생했을 때
- 요소의 스타일(색상, 배경색 등)이 변경되었을 때
- visibility: hidden에서 visible로 변경될 때
최적화하기
리플로우 횟수가 많아지면 성능이 저하된다
다음과 같은 방법을 이용해보자.
1. Element.style.cssText로 style 조절
// 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로 대체
cssText는 DOM 요소의 style 속성에 여러 CSS 스타일을 한번에 문자열로 할당할 수 있는 속성이다.
따라서 여러번의 스타일 변경을 한 번에 처리하므로 리플로우가 한 번만 일어난다.
2. 동적으로 요소를 추가할 때 가상의 상위 요소 추가
// 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 요소 조작
bad 예시에선 반복문을 통해 dom 요소에 10번의 조작이 이뤄진다. 따라서 리플로우도 10번 이뤄진다.
good 예시에선 매 반복문 시행마다 dom 요소에 접근하는 게 아닌, 문자열 조작을 통해 메모리에서 변경사항이 이뤄지도록 한다. 그리고 한 번에 DOM 요소를 조작한다.
3. Fragment 태그로 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);
DocumentFragment 객체는 DOM 노드의 일종으로, 실제 메인 DOM 트리에 포함되지 않는 가상의 메모리 공간에 존재하는 객체이다. 마치 DOM 노드들을 담는 임시 저장소와 같다고 생각할 수 있다.
모든 조작을 그 안에서 하고 마지막에는 Fragment만 실제 DOM에 추가한다.
4. CSS 하위 선택자 최소화하기
<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가 깊어져서 렌더 트리를 만드는 시간이 더 오래 걸린다.
5. 계산된 css 활용 시 변수에 저장
// 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`;
}
브라우저는 Layout 변경을 큐에 저장했다가 한 번에 실행하여 reflow를 최소화한다. 하지만 offset, scrollTop과 같은 계산된 스타일 정보는 정확한 정보를 제공하기 위해 매번 큐를 비우고 모든 변경 사항을 적용하므로 reflow가 여러 번 일어난다.
위의 코드와 같이 스타일 정보를 변수에 저장하고 한 번에 처리하는 방식으로 offset, scrollTop 등의 값 요청을 최소화할 수 있다.
(2번처럼 메모리에서 조작)
6. animation 노드에 position: fixed 또는 absoluted 부여
애니메이션 효과는 relow 비용이 크므로,
애니메이션 노드에 position: fixed나 absolute 속성을 주면 해당 노드를 전체 노드에서 분리시켜서 그 노드에서만 reflow가 발생할 수 있다.
-> 애니메이션이 시작할 때 positon을 fixed 또는 absolute로 변경,
애니메이션이 종료된 후 다시 원상 복구
7. 되도록 하위 노드의 클래스에서 css 변경
상위 노드의 크기 또는 위치를 변경하면 하위노드에까지 영향이 가므로,
가장 하위 노드의 스타일을 변경해 리플로우 최소화
8. inline 스타일 사용하지 않기
인라인 스타일은 HTML 요소의 style 속성을 사용하여 스타일을 지정하는 방법이다.
HTML을 파싱하다가 inline style을 사용한 태그를 만날 때마다 CSSOM이 재생성된다.
9. table을 이용한 레이아웃 피하기
<table>은 데이터가 모두 load되고 나서 + 테이블 너비가 계산된 후에 화면에 그려진다.
(테이블 안의 컨텐츠 값에 따라 테이블 너비가 계산되기 때문)
따라서 테이블 컨텐츠의 작은 변경만 있어도 테이블 너비가 다시 계산되고, 테이블의 모든 노드에 reflow가 발생한다.
10. display:none 활용(숨겨진 노드의 css 변경)
display:none인 노드를 변경할 때는 reflow가 발생하지 않는 점을 이용한다.
다음과 같이 다수의 스타일을 변경하거나
const element = document.getElementById('target');
// 1. 요소를 렌더 트리에서 제외
element.style.display = 'none';
// 2. 리플로우 없이 다중 스타일 변경
element.style.width = '200px';
element.style.height = '100px';
element.style.margin = '20px';
// 3. 변경 완료 후 다시 표시 (리플로우 1회 발생)
element.style.display = 'block';
대량 DOM 조작 시 활용할 수 있다.
const list = document.getElementById('list');
// 1. 리스트 숨기기
list.style.display = 'none';
// 2. 리플로우 없이 100개 항목 추가
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
list.appendChild(fragment);
// 3. 리스트 표시 (리플로우 1회)
list.style.display = 'block';
참고
브라우저 reflow와 repaint - 최적화
reflow와 repaint의 최적화 방법에 대해 알아보자.
velog.io
'Web' 카테고리의 다른 글
| 블로킹, 논블로킹 vs 동기, 비동기 (0) | 2025.03.26 |
|---|---|
| HTTP 캐싱과 캐싱 제어 (0) | 2025.03.06 |
| 브라우저 렌더링 작동 원리: 파싱부터 렌더링까지 (0) | 2025.02.26 |
| "웹페이지를 표시한다는 것: 브라우저는 어떻게 동작하는가" (0) | 2025.02.13 |
| 세션 인증과 토큰 인증 (1) | 2025.02.11 |