STUDY NOTE · 개념 정리
브라우저 렌더링 과정 정리: Critical Rendering Path, Reflow와 Repaint
Frontend
브라우저는 HTML로 DOM, CSS로 CSSOM을 만들고, 둘을 합친 렌더 트리로 위치와 크기를 계산(Layout)한 뒤, 픽셀을 칠하고(Paint) 레이어를 합쳐(Composite) 화면에 그린다. 레이아웃부터 다시 하면 Reflow, 칠하기만 다시 하면 Repaint, 합성만 다시 하면 가장 싸다.
1. 전체 흐름
- HTML 파싱 → DOM: 바이트 → 문자 → 토큰 → 노드 → 트리
- CSS 파싱 → CSSOM: 선택자와 스타일 규칙을 트리로
- 렌더 트리(Render Tree): DOM + CSSOM에서 화면에 보이는 노드만 (
display: none,<head>제외) - Layout (Reflow): 각 노드의 정확한 위치와 크기 계산
- Paint: 글자, 색, 테두리, 그림자 등을 레이어별로 픽셀로 칠할 목록 만들기
- Composite: 레이어를 순서대로 합쳐 화면에 표시 (GPU가 담당)
이 1~6까지, 첫 화면이 그려지기까지 꼭 거쳐야 하는 경로를 Critical Rendering Path(CRP)라고 한다.
2. 렌더링을 막는 것들
| 리소스 | 파싱 차단 | 렌더링 차단 | 설명 |
| CSS | 아니오 | 예 | CSSOM이 없으면 렌더 트리를 못 만듦. 그래서 head에 두고 작게 |
| 일반 script | 예 | 예 | 만나는 순간 HTML 파싱을 멈추고 내려받아 실행 (JS가 DOM을 바꿀 수 있어서) |
| script async | 아니오 (실행 순간만 멈춤) | - | 받는 대로 실행, 순서 보장 안 함 (광고, 분석 스크립트) |
| script defer | 아니오 | - | 파싱이 끝난 뒤 문서 순서대로 실행 (DOMContentLoaded 직전) |
| type="module" | 아니오 | - | 기본이 defer처럼 동작 |
| img | 아니오 | 아니오 | 단, 크기를 안 정해 두면 로드 후 레이아웃이 밀림 (CLS) |
또 하나: JS가 스타일 값을 읽으려면 CSSOM이 필요하므로, CSS가 다 오기 전까지는 그 뒤의 스크립트 실행도 기다린다.
3. Reflow vs Repaint vs Composite
| 구분 | 다시 하는 단계 | 일어나는 변경 | 비용 |
| Reflow (Layout) | Layout → Paint → Composite | width, height, margin, padding, top/left, font-size, 요소 추가·삭제, 창 크기 변경 | 가장 큼 (자식·형제까지 다시 계산될 수 있음) |
| Repaint | Paint → Composite | color, background-color, visibility, box-shadow | 중간 |
| Composite만 | Composite | transform, opacity (레이어로 분리된 경우) | 가장 작음 (GPU에서 처리) |
애니메이션은 left, top 대신 transform: translate(), 사라지는 효과는 display나 visibility 대신 opacity를 쓰는 이유가 이것이다.
4. 강제 동기 레이아웃 (Layout Thrashing)
브라우저는 스타일 변경을 모아 뒀다가 다음 프레임에 한 번에 레이아웃을 계산한다. 그런데 중간에 offsetHeight, getBoundingClientRect(), scrollTop 같은 레이아웃 값을 읽으면, 정확한 값을 주려고 그 자리에서 바로 레이아웃을 계산한다.
// 나쁨: 읽기와 쓰기를 번갈아서 매 반복마다 레이아웃 강제
for (const el of items) {
el.style.width = box.offsetWidth + "px"; // 읽기(강제 레이아웃) → 쓰기
}
// 좋음: 읽기를 먼저 모으고, 쓰기를 한꺼번에
const w = box.offsetWidth;
for (const el of items) {
el.style.width = w + "px";
}
- 쓰기를 모으려면
requestAnimationFrame안에서 처리하거나, 클래스 하나를 바꿔 스타일을 한 번에 적용한다. - 많은 요소를 추가할 땐
DocumentFragment에 모아서 한 번에 붙인다.
5. 레이어와 합성
transform,opacity애니메이션,will-change,position: fixed,<video>,<canvas>등은 별도 합성 레이어로 올라갈 수 있다.- 레이어로 분리된 요소는 움직여도 다른 요소를 다시 칠하지 않는다. 그렇다고 레이어를 남발하면 GPU 메모리를 많이 먹는다.
will-change는 실제로 움직이기 직전에만 쓴다.
6. 성능 지표와 연결
| 지표 | 의미 | 관련 단계 |
| FCP | 첫 콘텐츠가 그려진 시점 | CRP 전체 (렌더링 차단 리소스 줄이기) |
| LCP | 가장 큰 콘텐츠가 그려진 시점 | 큰 이미지·글꼴 로딩, 서버 응답 |
| CLS | 레이아웃이 예기치 않게 밀린 정도 | 크기 없는 이미지, 늦게 끼어드는 광고·글꼴 |
| INP | 입력에 대한 다음 화면 반응 시간 | 긴 JS 작업, 무거운 Reflow |
7. 정리
- DOM + CSSOM → 렌더 트리 → Layout → Paint → Composite.
- CSS는 렌더링을, 일반 script는 파싱을 막는다. script는
defer가 기본 선택. - 비용은 Reflow > Repaint > Composite. 움직임은
transform과opacity로. - 레이아웃 값을 읽고 쓰는 걸 섞지 않는다.