본문으로 건너뛰기
yutils

브라우저는 페이지를 어떻게 그릴까?

HTML bytes 가 pixel 이 되기까지 — parsing · DOM/CSSOM · render tree · layout · paint · composite. reflow 가 비싼 이유, GPU layer 가 생기는 trigger, 60fps 의 16.67ms 예산 안에 머무는 법.

약 8분 읽기

HTML byte 가 화면의 pixel 이 되기까지 무슨 일이 일어나나? 단순 "render" 가 아니라 parsing → DOM/CSSOM → render tree → layout → paint → composite 의 pipeline. 어디서 느려지는지 알아야 60fps (frame 당 16.67ms) 예산 안에 머문다. 이 가이드는 브라우저 렌더링의 6 단계, reflow vs repaint 의 차이, GPU layer 의 동작을 정리한다.

Critical Rendering Path — 6 단계

HTML bytes
   ↓ parse
DOM tree
   ↓ + CSS parse
CSSOM tree
   ↓ combine
Render tree    (DOM + 적용된 style, display:none element 제외)
   ↓ layout
Geometry (각 box 의 x/y/width/height)
   ↓ paint
Pixel data
   ↓ composite
화면

Step 1-2 — Parsing (DOM + CSSOM)

브라우저가 byte stream 받아 token → node → tree:

  • HTML parser 가 DOM 만듦 (how-html-parsing-works 가이드 참조)
  • CSS parser 가 CSSOM 만듦
  • CSSOM 은 render-blocking — CSS 안 받으면 렌더 X (FOUC 회피)
  • script 는 default 로 parser-blocking — DOM 만들기 멈추고 script 실행. async / defer 로 회피
<script src="..."></script>        ← parser blocking
<script async src="..."></script>  ← download 병행, 받자마자 실행
<script defer src="..."></script>  ← download 병행, DOMContentLoaded 직전 실행

Step 3 — Render Tree

DOM + CSSOM 결합. 단, 화면에 표시되는 element 만:

  • display: none 제외
  • visibility: hidden 는 포함 (자리는 차지)
  • head 의 자식은 모두 제외

Step 4 — Layout (= reflow)

각 box 의 정확한 위치·크기 계산:

  • document flow, box model (margin/border/padding), float, position
  • flex / grid 계산
  • text wrap, line break

매우 비쌈 — 한 element 의 size 변경이 자식 + 형제 + 부모 layout 까지 chain. 큰 페이지는 수십 ms.

Layout 을 trigger 하는 변경:

width / height / top / left / right / bottom
margin / padding / border
font-size / line-height
display / position / float
↓
모두 reflow trigger

Step 5 — Paint

Geometry 가 결정된 후 실제 pixel 그림:

  • background color / image
  • border style
  • text rendering (font glyph 합성)
  • box-shadow / outline

Paint 만 발생하는 변경 (layout 무관):

color
background
visibility
outline
box-shadow
↓
geometry 변경 X → layout skip, paint 만

Step 6 — Composite

Paint 한 layer 들을 GPU 에서 합성:

  • element 마다 다른 layer 가능 → GPU 가 transform / opacity 만 처리
  • CPU 부담 ↓, frame rate ↑

GPU 가 처리하는 변경 (layout / paint 둘 다 skip):

transform: translate / scale / rotate
opacity
↓
60fps 자연 보장. 가장 cheap 한 animation.

3 가지 변경의 cost

변경Pipeline비용
width, height, topLayout → Paint → Composite비쌈 ❌
color, backgroundPaint → Composite중간
transform, opacityComposite 만가장 싸다 ✅

Animation 은 항상 transform / opacity. top 으로 움직이지 말 것.

60fps 의 16.67ms 예산

1000 ms / 60 = 16.67 ms per frame

여기서 빠질 것:
- browser overhead (input event 처리 등): ~2-3 ms
- 남은 ~13 ms 가 JavaScript + 렌더링

목표:
- JavaScript: < 8 ms
- Style / Layout / Paint / Composite: < 5 ms

초과 시 → frame drop → jank 느낌

GPU Layer 만들기 — will-change

/* hover 시 부드러운 transform */
.card {
  transition: transform 0.3s;
  will-change: transform;     ← 미리 GPU layer 만듦
}

.card:hover {
  transform: scale(1.05);
}

will-change 가 미리 layer 준비. 단, 남용 X — 모든 element 에 박으면 memory 폭주.

암묵적 GPU layer trigger:

  • transform: translateZ(0) 또는 translate3d
  • position: fixed
  • <video> / <canvas>
  • opacity < 1 + animation
  • will-change: transform | opacity

Layout Thrashing — 가장 흔한 함정

// Bad
for (let i = 0; i < boxes.length; i++) {
  boxes[i].style.width = boxes[i].offsetWidth + 10 + 'px';
  //                                ↑ read           ↑ write
  // read forces layout → write invalidates → next read forces again
  // → N 회 layout
}

// Good — read 와 write 분리
const widths = boxes.map(b => b.offsetWidth);   // 모든 read 먼저
boxes.forEach((b, i) => b.style.width = widths[i] + 10 + 'px');
// → 1 회 layout

offsetWidth, getBoundingClientRect, scrollTop 등 — geometry read API. 직전 write 후 호출 시 layout 강제 (= forced synchronous layout).

Core Web Vitals 와 rendering

  • LCP (Largest Contentful Paint) — 가장 큰 element render 시간. 2.5s 이하 권장.
  • FID / INP (Interaction to Next Paint) — 사용자 input → next frame 시간. 200ms 이하 권장.
  • CLS (Cumulative Layout Shift) — 갑작스러운 layout 이동 누적 점수. 0.1 이하 권장.

CLS 의 흔한 원인 — image 의 width/height 미지정 → 로드 후 자리 차지하며 다른 element 밀어냄. aspect-ratio 또는 width + height 명시.

흔한 함정

1. animation 의 top/left

Layout 매 frame trigger. transform 으로 교체.

2. 큰 box-shadow + animation

shadow 가 paint 비용 ↑. 큰 element 의 hover shadow 는 60fps 어려움.

3. forced sync layout

read 와 write 섞기. requestAnimationFrame 으로 분리.

4. font 의 FOIT

font-display: swap 없이 webfont 로드 → 폰트 받는 동안 invisible. swap 으로 fallback 표시 후 교체.

5. excessive will-change

모든 element 에 박으면 메모리 폭주. 실제로 animate 되는 element 만.

참고 자료

요약

  • Render pipeline 6 단계 — parse → DOM/CSSOM → render tree → layout → paint → composite.
  • Layout (reflow) 가 가장 비쌈. width/height/top/left 변경 시 trigger.
  • Paint 만 — color/background/visibility/box-shadow.
  • Composite 만 (GPU) — transform/opacity. 60fps animation 의 표준.
  • 60fps 예산 = frame 당 16.67ms. JS 8ms + 렌더 5ms 안에.
  • will-change 로 미리 GPU layer. 남용 X.
  • Layout thrashing = read/write 교차로 매번 layout. read 먼저 batch.
  • Core Web Vitals (LCP/INP/CLS) — image 크기 명시, animation 은 transform, font-display: swap.
가이드 목록으로