Computer Science 목록으로

브라우저 렌더링 과정

Computer Science

들어가며

프론트엔드 개발을 하다 보면 이런 말을 자주 듣는다. "이 CSS 쓰면 성능이 좋아져요." "레이아웃 스래싱이 생겼어요." "GPU 가속을 써야 해요." 그런데 왜 그런지, 어디서 차이가 나는 건지 제대로 설명할 수 있는가?

브라우저가 화면을 그리는 과정을 모르면 이 모든 게 주술처럼 들린다. Critical Rendering Path(CRP) 는 URL 입력부터 첫 픽셀이 화면에 찍히기까지의 과정이다. 이걸 이해하면 transform이 왜 top보다 빠른지, display: none이 왜 visibility: hidden과 다른지, defer가 왜 성능에 영향을 주는지가 전부 논리적으로 연결된다.



HTML을 받았다, 그 다음은?

브라우저가 서버로부터 HTML 바이트를 받으면 크게 두 가지 파싱이 동시에 시작된다.

  • HTML 파서 : HTML -> DOM 트리
  • CSS 파서 : CSS -> CSSOM 트리

이 둘이 합쳐져야 비로소 화면을 그릴 수 있다. 그런데 여기에 방해꾼이 있다. 바로 <script> 태그다.

HTML
1<!-- 파서가 여기서 멈춘다 --> 2<script src="app.js"></script>

HTML 파서는 <script>를 만나는 순간 파싱을 중단한다. JS가 DOM을 바꿀 수 있기 때문에, JS 실행이 끝나야 파싱을 재개한다. 이것이 defer와 async가 존재하는 이유다.

HTML
1<!-- defer: HTML 파싱 완료 후 실행, 순서 보장 --> 2<script src="app.js" defer></script> 3 4<!-- async: 다운로드 완료 즉시 실행, 순서 미보장 --> 5<script src="analytics.js" async></script>


DOM 트리

브라우저는 HTML 바이트를 받아 아래 과정을 거친다.

바이트 -> 문자 -> 토큰 -> 노드 -> DOM트리



CSSOM 트리

CSS는 HTML과 별도로 파싱되어 CSSOM트리를 만든다. 중요한 점은 CSSOM은 렌더링을 차단한다는 것이다.

브라우저는 CSS가 완전히 파싱될 때까지 렌더링을 시작하지 않는다. 이유는 명확하다. CSS는 계단식으로 적용되기 때문에, CSSOM이 완성되지 않으면 어떤 스타일을 적용해야 할지 알 수 없다.

따라서 <link rel="stylesheet">는 최대한 <head> 상단에 두어 빠르게 파싱되게 해야 한다.



Render Tree

DOM과 CSSOM이 완성되면 브라우저는 둘을 합쳐 Render Tree를 만든다. 이 과정에서 핵심 규칙이 하나 있다. 화면에 표시되지 않는 노드는 Render Tree에 포함되지 않는다.

이미지

display: none은 해당 노드와 그 하위 노드 전체를 Render Tree에서 제거한다. 반면 visibility: hidden은 Render Tree에 포함되어 공간은 차지하되 투명하게 보인다. 이 차이가 성능에 직접적인 영향을 미친다.



Layout(Reflow)

Render Tree가 만들어지면 브라우저는 각 노드의 정확한 위치와 크기를 계산한다. 이 단계를 Layout 또는 Reflow라고 부른다. 이 때, 뷰포트를 기준으로 모든 요소의 px 좌표가 결정된다.

Reflow는 비용이 크다. 한 요소의 크기가 바뀌면 부모, 형제, 자식 요소까지 연쇄적으로 재계산이 일어나기 때문이다.

레이아웃 스래싱(Layout Thrashing) 은 읽기와 쓰기가 교차 반복될 때 발생한다.

JAVASCRIPT
1// 나쁜 예: 읽기-쓰기가 반복 → Reflow가 매번 강제 발생 2elements.forEach(el => { 3 const h = el.offsetHeight; // 읽기 → Reflow 강제 4 el.style.height = h + 10 + 'px'; // 쓰기 5}); 6 7// 좋은 예: 읽기를 먼저 모아두고 쓰기를 나중에 8const heights = elements.map(el => el.offsetHeight); // 읽기만 9elements.forEach((el, i) => el.style.height = heights[i] + 10 + 'px'); // 쓰기만


Paint

Layout이 끝나면 이제 실제로 픽셀을 채운다. 텍스트, 색상, 이미지, 테두리, 그림자 같은 시각적 속성들이 이 단계에서 처리된다.

Repaint는 Reflow보다 비용이 작지만, 여전히 CPU를 사용하기 때문에 과도하면 성능 문제가 된다.



Composite

Paint된 레이어들을 최종적으로 화면에 합성하는 단계다. 이 작업은 GPU가 담당한다.

여기서 핵심이 나온다. transform과 opacity가 빠른 이유는 이 단계만 거치기 때문이다.

CSS
1/* Reflow → Paint → Composite (비쌈) */ 2.slow { left: 100px; } 3 4/* Composite만 (GPU가 처리, 매우 빠름) */ 5.fast { transform: translateX(100px); }


마치며

이론을 코드와 연결하는 가장 빠른 방법은 Chrome DevTools의 Performance 탭이다.

  1. DevTools 열기 (F12)
  2. Performance 탭 → Record
  3. 페이지에서 인터랙션 수행
  4. Stop → 타임라인 확인

타임라인에서 보라색 블록은 Layout(Reflow), 초록색 블록은 Paint를 의미한다. 애니메이션 중에 이 블록들이 보인다면 최적화가 필요하다는 신호다. transform/opacity만 사용하는 애니메이션에는 보라색·초록색 블록이 없고, Composite만 일어나는 것을 확인할 수 있다.