Dev Notes
Web Vitals
Web Vitals
Dev Notes
Docs브라우저 렌더링 파이프라인
Web Vitals
Web Vitals
GitHub
Web57분

브라우저 렌더링 파이프라인

HTML을 화면에 그리기까지의 전체 과정과 프론트엔드 성능 최적화의 근본 원리

2026년 1월 30일
browserrenderingcritical-rendering-pathperformanceDOMCSSOMreflowrepaintcompositing

적용 환경: 모든 모던 브라우저 (Chromium 기반 설명, 원리는 동일)

들어가며

DevTools의 Performance 탭을 열고 프로파일링을 돌리면, 노란색(Scripting), 보라색(Rendering), 초록색(Painting) 바들이 복잡하게 얽힌 타임라인을 마주하게 됩니다. 이 바들이 각각 무엇을 의미하는지, 어떤 코드가 어떤 바를 트리거하는지를 이해하지 못하면 성능 병목을 잡아내는 것은 추측에 의존할 수밖에 없습니다.

브라우저가 HTML 문서를 받아서 화면에 픽셀을 출력하기까지는 정해진 파이프라인을 거칩니다. 이 파이프라인의 각 단계를 이해하면 "왜 이 CSS 속성을 바꾸면 느려지는지", "왜 JavaScript가 렌더링을 차단하는지", "왜 transform이 left보다 빠른지"에 대한 근본적인 답을 얻을 수 있습니다. Core Web Vitals 가이드에서 다루는 LCP, INP, CLS의 최적화 전략 역시 이 파이프라인에 대한 이해 위에서 비로소 납득이 됩니다.

렌더링 파이프라인 전체 흐름

브라우저가 HTML을 화면에 그리기까지 거치는 과정을 Critical Rendering Path라 부릅니다. "Critical"이라는 이름이 붙은 이유는, 이 경로의 어떤 단계라도 차단되면 사용자에게 첫 픽셀이 보이는 시점이 그만큼 밀리기 때문입니다.

1단계

HTML 파싱

바이트 → 토큰 → DOM 트리

2단계

CSS 파싱

바이트 → 토큰 → CSSOM 트리

3단계

Render Tree

DOM + CSSOM 결합

4단계

Layout

각 노드의 크기와 위치 계산

5단계

Paint

픽셀 단위 그리기 명령 생성

6단계

Composite

레이어 합성 → 화면 출력

1단계

HTML 파싱

바이트 → 토큰 → DOM 트리

2단계

CSS 파싱

바이트 → 토큰 → CSSOM 트리

3단계

Render Tree

DOM + CSSOM 결합

4단계

Layout

각 노드의 크기와 위치 계산

5단계

Paint

픽셀 단위 그리기 명령 생성

6단계

Composite

레이어 합성 → 화면 출력

이 6단계는 초기 로드 시에만 실행되는 것이 아닙니다. JavaScript가 DOM이나 스타일을 변경할 때마다 파이프라인의 일부 또는 전부가 다시 실행됩니다. 어떤 변경이 어느 단계부터 다시 실행하는지를 아는 것이 프론트엔드 성능 최적화의 핵심이며, 이 문서의 중심 주제이기도 합니다.

HTML 파싱과 DOM 트리 구축

브라우저가 서버로부터 HTML을 수신하면 가장 먼저 파서(parser)가 동작합니다. HTML 파싱은 네 단계를 거쳐 DOM 트리를 구축합니다.

  1. 바이트 → 문자: 네트워크로 수신한 바이트를 charset에 따라 문자로 디코딩
  2. 문자 → 토큰: <html>, <head>, <body> 같은 시작/종료 태그를 토큰으로 분리
  3. 토큰 → 노드: 각 토큰을 속성과 규칙이 있는 노드 객체로 변환
  4. 노드 → DOM 트리: 부모-자식 관계에 따라 트리 구조로 조립
HTML
<html>
  <head>
    <link rel="stylesheet" href="style.css" />
    <script src="app.js"></script>
  </head>
  <body>
    <h1>제목</h1>
    <p>본문</p>
  </body>
</html>

이 HTML에서 파서는 <html>을 루트로, <head>와 <body>를 자식으로, <h1>과 <p>를 <body>의 자식으로 배치합니다. 여기서 중요한 특성이 두 가지 있습니다.

첫째, HTML 파싱은 점진적(incremental)입니다. 전체 HTML 다운로드가 끝나기를 기다리지 않고, 수신되는 대로 파싱을 시작합니다. 이 덕분에 사용자는 전체 페이지가 로드되기 전에도 일부 콘텐츠를 볼 수 있습니다.

둘째, <script> 태그를 만나면 파싱이 중단됩니다. JavaScript는 document.write()로 HTML 구조를 바꾸거나, DOM을 직접 조작할 수 있으므로, 스크립트 실행이 끝나기 전까지 파서가 이후의 HTML을 안전하게 처리할 수 없기 때문입니다. 이것이 "JavaScript가 렌더링을 차단한다"는 말의 실체이며, defer와 async 속성이 존재하는 이유입니다.

한편 메인 파서가 <script> 실행을 기다리는 동안, 프리로드 스캐너(preload scanner)라는 별도의 경량 파서가 나머지 HTML을 훑으며 이미지, 스타일시트, 폰트 등의 리소스를 미리 발견하고 다운로드를 시작합니다. 프리로드 스캐너가 없다면 스크립트 하나가 실행되는 동안 모든 리소스 다운로드가 멈추게 되므로, 이 최적화는 실제 페이지 로드 속도에 상당한 영향을 미칩니다.

CSS 파싱과 CSSOM 트리

HTML 파서가 <link rel="stylesheet">를 발견하면 CSS 파일의 다운로드가 시작됩니다. 다운로드된 CSS는 HTML과 유사하게 바이트 → 토큰 → 노드 → 트리의 과정을 거쳐 CSSOM(CSS Object Model) 트리로 구축됩니다. CSSOM은 각 노드에 적용되는 스타일 규칙을 담고 있으며, cascade, specificity, inheritance 계산이 이 단계에서 수행됩니다.

DOM과 CSSOM의 핵심적인 차이점은 구축 방식에 있습니다. DOM은 점진적으로 구축되어 일부만 완성되어도 렌더링을 시작할 수 있지만, CSSOM은 전체가 완성되어야 다음 단계로 넘어갑니다. 이는 CSS의 cascade 특성 때문입니다. 스타일시트 뒤쪽에 나오는 규칙이 앞선 규칙을 덮어쓸 수 있으므로, 전체를 파싱하기 전에는 최종 스타일을 확정할 수 없습니다.

이 때문에 CSS는 렌더 차단 리소스(render-blocking resource)로 분류됩니다. CSSOM 구축이 완료되지 않으면 브라우저는 렌더링 자체를 시작하지 않습니다. 만약 CSSOM 없이 렌더링을 시작한다면, 스타일이 적용되지 않은 날것의 HTML이 잠깐 보였다가 스타일이 뒤늦게 적용되면서 화면이 번쩍이는 FOUC(Flash of Unstyled Content) 현상이 발생하게 됩니다. 브라우저는 이 경험을 방지하기 위해 의도적으로 CSSOM 완료를 기다립니다.

CSS는 렌더 차단 리소스

CSS 파일이 크거나 네트워크가 느리면, CSSOM 구축이 늦어져 사용자는 빈 화면을 오래 보게 됩니다. 이것이 Core Web Vitals의 LCP와 FCP를 지연시키는 핵심 원인 중 하나입니다. Critical CSS를 인라인으로 <head>에 삽입하고 나머지를 비동기 로드하는 전략이 효과적인 이유가 여기에 있습니다.

Render Tree 생성

DOM과 CSSOM이 모두 준비되면, 브라우저는 이 둘을 결합하여 Render Tree를 생성합니다. Render Tree는 "실제로 화면에 그려야 할 노드"만 포함하는 트리입니다.

DOM의 모든 노드가 Render Tree에 포함되는 것은 아닙니다. <head>, <script>, <meta> 같은 비시각적 요소는 제외됩니다. CSS로 display: none이 적용된 요소 역시 Render Tree에서 완전히 제외되어 공간을 차지하지 않습니다.

반면 visibility: hidden은 다르게 동작합니다. 이 속성이 적용된 요소는 Render Tree에 포함되어 공간을 차지하지만, 화면에 보이지는 않습니다. 레이아웃 계산에 영향을 미치느냐의 차이이므로, 성능 관점에서 이 구분은 중요합니다.

CSS
/* Render Tree에서 제외 → 공간 차지 안 함, Layout에 영향 없음 */
.removed {
  display: none;
}
 
/* Render Tree에 포함 → 공간 차지함, 보이지만 않음 */
.invisible {
  visibility: hidden;
}

흥미로운 점은 ::before와 ::after 같은 CSS 의사 요소입니다. 이들은 DOM에는 존재하지 않지만 CSSOM에 스타일 규칙이 정의되어 있으므로, Render Tree에는 포함됩니다. 즉 Render Tree는 "DOM 트리의 부분집합"이 아니라, DOM과 CSSOM이 결합된 별도의 트리라고 이해하는 것이 정확합니다.

Layout (Reflow)

Render Tree에는 "무엇을 그릴지"와 "어떤 스타일을 적용할지"가 담겨 있지만, "화면의 어디에, 얼마나 크게 그릴지"는 아직 결정되지 않았습니다. 이 기하학적 정보를 계산하는 단계가 Layout(또는 Reflow)입니다.

Layout은 Render Tree의 루트 노드부터 시작하여 자식 노드로 재귀적으로 내려가며, 각 노드의 정확한 위치와 크기를 계산합니다. CSS Box Model(content, padding, border, margin)이 이 단계에서 실체화되며, % 단위는 부모의 크기를 기준으로 픽셀 값으로 변환됩니다.

Layout은 파이프라인에서 가장 비용이 큰 단계 중 하나입니다. 하나의 노드 크기가 바뀌면 자식 노드뿐 아니라 형제 노드, 때로는 부모 노드까지 연쇄적으로 재계산이 필요하기 때문입니다. 이 연쇄적 재계산이 일어나는 대표적인 경우가 Layout Thrashing(강제 동기 레이아웃)입니다.

JavaScript
// ❌ Layout Thrashing: 읽기-쓰기를 반복하면 매번 강제 레이아웃 발생
function resizeAll(elements) {
  elements.forEach(el => {
    const width = el.offsetWidth;        // 읽기 → 레이아웃 강제 실행
    el.style.width = (width * 2) + 'px'; // 쓰기 → 레이아웃 무효화
  });
}
 
// ✅ 읽기를 모두 먼저 수행한 후 쓰기를 일괄 처리
function resizeAll(elements) {
  const widths = elements.map(el => el.offsetWidth); // 읽기 일괄
  elements.forEach((el, i) => {
    el.style.width = (widths[i] * 2) + 'px';         // 쓰기 일괄
  });
}

첫 번째 코드에서 offsetWidth를 읽는 순간, 브라우저는 직전에 변경된 스타일을 반영하기 위해 레이아웃을 강제로 다시 실행합니다. 루프 안에서 이 읽기-쓰기가 반복되면 레이아웃이 요소 수만큼 실행되므로, 요소가 100개이면 레이아웃도 100번 발생합니다. 두 번째 코드처럼 읽기와 쓰기를 분리하면 레이아웃은 한 번만 실행됩니다.

Layout을 트리거하는 CSS 속성들을 알아두면 불필요한 Reflow를 피하는 데 도움이 됩니다.

카테고리속성
크기width, height, padding, margin, border-width
위치top, left, right, bottom, position
텍스트font-size, font-family, line-height, text-align
박스 모델display, overflow, float, flex, grid

CSS 속성별 트리거 확인

각 CSS 속성이 Layout, Paint, Composite 중 어떤 단계를 트리거하는지는 web.dev의 렌더링 성능 가이드와 MDN의 CSS 애니메이션 성능에서 확인할 수 있습니다.

Paint와 Paint Order

Layout이 "어디에, 얼마나 크게"를 결정했다면, Paint 단계는 "어떤 색으로, 어떤 순서로 그릴지"를 결정합니다. Paint는 실제 픽셀을 화면에 찍는 것이 아니라, 그리기 명령 목록(display list)을 생성하는 단계입니다. "이 좌표에 이 색으로 사각형을 그려라", "이 위치에 이 텍스트를 렌더링하라" 같은 명령들이 순서대로 기록됩니다.

그리기 순서는 CSS의 stacking context에 따라 결정되며, 대략 다음과 같습니다.

  1. 배경색(background-color)
  2. 배경이미지(background-image)
  3. 테두리(border)
  4. 자식 요소
  5. 아웃라인(outline)

Paint 단계에서 주목할 점은, 기하학적 변경 없이 시각적 속성만 바꾸면 Layout을 건너뛰고 Paint부터 다시 실행된다는 것입니다. 이를 Repaint라 부르며, Reflow보다 비용이 낮습니다.

속성설명
color텍스트 색상 변경
background-color배경색 변경
visibility가시성 토글 (공간 유지)
box-shadow그림자 변경
outline아웃라인 변경
border-radius모서리 둥글기 변경
border-color테두리 색상 변경

이 속성들은 요소의 크기나 위치를 바꾸지 않으므로 Layout을 트리거하지 않습니다. 그러나 복잡한 그라디언트, 큰 box-shadow, CSS filter 같은 시각 효과는 Paint 비용 자체가 높을 수 있으므로, DevTools의 Paint Profiler로 실제 비용을 확인하는 것이 좋습니다.

레이어 합성과 GPU 가속

Paint까지 완료되면 마지막 단계인 Composite(합성)에서 그려진 레이어들을 하나의 화면으로 조합합니다. 이 단계는 메인 스레드가 아닌 Compositor Thread에서 실행되며, GPU가 담당합니다. 메인 스레드와 독립적으로 동작한다는 점이 성능 최적화에서 결정적인 의미를 가집니다.

브라우저는 페이지를 여러 개의 레이어(layer)로 분리합니다. 레이어가 존재하는 이유는 단순합니다. 페이지의 일부만 변경되었을 때 전체를 다시 그리지 않고 해당 레이어만 다시 그린 뒤 합성하면 되기 때문입니다. 다음과 같은 조건에서 브라우저는 별도의 레이어를 생성합니다.

  • transform 또는 opacity에 애니메이션이 적용된 요소
  • will-change: transform 또는 will-change: opacity가 선언된 요소
  • position: fixed 또는 position: sticky
  • <video>, <canvas>, <iframe> 요소
  • CSS filter가 적용된 요소
Composite
← 가장 저렴
Paint
← 중간
Layout
← 가장 비싼

이 비용 구조가 프론트엔드 애니메이션 성능의 핵심입니다. transform과 opacity는 Composite 단계에서만 처리되므로 Layout과 Paint를 전혀 트리거하지 않습니다. 반면 left, top, width, height 같은 속성은 Layout부터 다시 시작하므로 파이프라인 전체를 재실행하게 됩니다.

CSS
/* ❌ Layout + Paint + Composite 모두 트리거 */
.animate-bad {
  transition: left 0.3s, top 0.3s;
}
.animate-bad:hover {
  left: 100px;
  top: 50px;
}
 
/* ✅ Composite만 트리거 (GPU 가속) */
.animate-good {
  transition: transform 0.3s;
}
.animate-good:hover {
  transform: translate(100px, 50px);
}

시각적 결과는 동일하지만, 두 번째 코드는 메인 스레드를 차단하지 않기 때문에 60fps를 안정적으로 유지할 수 있습니다. 특히 JavaScript로 무거운 작업이 메인 스레드에서 동시에 실행되고 있어도, Compositor Thread에서 독립적으로 애니메이션이 처리되므로 끊김 없이 동작합니다.

will-change 남용 주의

will-change는 브라우저에게 변경될 속성을 미리 알려 별도의 레이어를 생성하게 합니다. 그러나 모든 요소에 적용하면 레이어 수가 급증하여 메모리 사용량이 크게 늘어납니다. 실제로 애니메이션이 적용되는 요소에만 선별적으로 사용해야 하며, 애니메이션이 끝나면 will-change: auto로 되돌리는 것이 바람직합니다.

Reflow와 Repaint

지금까지 살펴본 파이프라인은 초기 로드 시 한 번만 실행되는 것이 아닙니다. JavaScript가 DOM이나 스타일을 변경할 때마다 파이프라인의 일부 또는 전부가 다시 실행됩니다. 이 재실행을 Reflow(Layout 재계산)와 Repaint(재그리기)로 구분합니다.

구분ReflowRepaint
다른 이름LayoutRedraw
발생 조건크기, 위치, 구조 변경시각적 속성만 변경
비용높음 (하위 노드로 전파)중간
후속 작업Paint → CompositeComposite
대표 트리거width, height, margin, appendChild()color, background, box-shadow

핵심 규칙은 Reflow는 항상 Repaint를 동반하지만, Repaint는 Reflow 없이 발생할 수 있다는 것입니다. color를 바꾸면 기하학적 변경이 없으므로 Repaint만 발생하지만, width를 바꾸면 Layout을 다시 계산한 후 Paint도 다시 실행해야 합니다.

브라우저는 성능을 위해 스타일 변경을 배치(batch)합니다. 여러 스타일 변경이 연속으로 발생하면, 다음 프레임 시점에 한 번에 반영합니다. 그러나 특정 DOM 속성을 읽는 순간, 브라우저는 최신 값을 반환하기 위해 대기 중이던 변경을 즉시 반영해야 하므로 강제 동기 레이아웃이 발생합니다.

다음 속성이나 메서드를 호출하면 브라우저는 최신 Layout 결과를 반환하기 위해 대기 중인 스타일 변경을 즉시 반영합니다.

offsetWidth, offsetHeight, offsetTop, offsetLeft, clientWidth, clientHeight, clientTop, clientLeft, scrollWidth, scrollHeight, scrollTop, scrollLeft, getComputedStyle(), getBoundingClientRect()

이 속성들을 루프 안에서 스타일 변경과 번갈아 호출하면 Layout Thrashing이 발생합니다.

DOM 변경이 여러 이벤트 핸들러에 걸쳐 산발적으로 발생하는 경우, requestAnimationFrame으로 다음 프레임에 일괄 적용하면 불필요한 중간 Reflow를 방지할 수 있습니다.

JavaScript
// ❌ 이벤트마다 Layout을 트리거하는 속성을 개별 변경
window.addEventListener('scroll', () => {
  items.forEach(item => {
    item.style.width = getNewWidth(item) + 'px';   // Layout 트리거
    item.style.height = getNewHeight(item) + 'px';  // Layout 트리거
  });
});
 
// ✅ requestAnimationFrame으로 다음 프레임에 일괄 적용
window.addEventListener('scroll', () => {
  requestAnimationFrame(() => {
    items.forEach(item => {
      item.style.width = getNewWidth(item) + 'px';
      item.style.height = getNewHeight(item) + 'px';
    });
  });
});

첫 번째 코드는 scroll 이벤트가 프레임당 여러 번 발생할 수 있으므로, 한 프레임 안에서 불필요한 중복 계산이 실행됩니다. 두 번째 코드는 requestAnimationFrame이 프레임당 한 번만 실행되므로, 동일한 시각적 결과를 최소한의 Layout 비용으로 달성합니다.

JavaScript와 렌더 차단

JavaScript는 렌더링 파이프라인에서 가장 복잡한 상호작용을 만들어내는 요소입니다. HTML 파서가 <script> 태그를 만나면 파싱을 중단하고, 스크립트의 다운로드와 실행이 완료될 때까지 기다립니다. JavaScript가 document.write()로 HTML 구조를 변경하거나, DOM API로 노드를 추가·삭제할 수 있기 때문에, 스크립트 실행 전에 이후의 HTML을 파싱하는 것은 안전하지 않습니다.

여기에 한 가지 복잡성이 더해집니다. JavaScript는 getComputedStyle()로 요소의 계산된 스타일을 읽을 수 있으므로, 스크립트 실행 전에 CSSOM이 완성되어 있어야 합니다. 이는 다음과 같은 의존 체인을 만듭니다.

CSS 다운로드/파싱 → JavaScript 실행 → HTML 파싱 재개

CSS 파일이 크면 CSSOM 구축이 늦어지고, 그만큼 JavaScript 실행이 지연되며, HTML 파싱 재개도 밀립니다. 렌더링 차단이 연쇄적으로 발생하는 구조입니다.

async와 defer

이 차단 문제를 완화하기 위해 <script> 태그에 async와 defer 속성이 도입되었습니다.

기본 script

파싱 중단

다운로드 → 실행 → 파싱 재개

async

병렬 다운로드

다운로드 완료 즉시 실행 (순서 보장 안 됨)

defer

병렬 다운로드

DOM 파싱 완료 후 순서대로 실행

module

defer 동작

기본적으로 defer + strict mode

기본 script

파싱 중단

다운로드 → 실행 → 파싱 재개

async

병렬 다운로드

다운로드 완료 즉시 실행 (순서 보장 안 됨)

defer

병렬 다운로드

DOM 파싱 완료 후 순서대로 실행

module

defer 동작

기본적으로 defer + strict mode

HTML
<!-- HTML 파싱 중단 → 다운로드 → 실행 → 파싱 재개 -->
<head>
  <script src="app.js"></script>
</head>

실무 가이드라인

대부분의 애플리케이션 스크립트에는 defer가 적합합니다. DOM에 의존하지 않는 독립적인 스크립트(Google Analytics, 광고 태그 등)에는 async를 사용합니다. ES Module(type="module")은 기본적으로 defer 동작을 하므로 별도 속성이 필요 없습니다.

CSS와 JavaScript의 상호 의존

실무에서 자주 간과되는 부분은 CSS와 JavaScript 사이의 의존 관계입니다. <link rel="stylesheet">가 <script> 앞에 위치하면, 스타일시트 다운로드가 완료되어 CSSOM이 구축될 때까지 스크립트 실행이 차단됩니다. 이 차단 체인이 성능에 미치는 영향은 생각보다 큽니다.

HTML
<head>
  <!-- style.css 다운로드 + 파싱이 끝나야 app.js가 실행됨 -->
  <link rel="stylesheet" href="style.css" />
  <script src="app.js"></script>
</head>

이 구조에서 style.css의 다운로드가 1초 걸리면, app.js의 실행도 1초 뒤로 밀리고, 그만큼 HTML 파싱 재개도 지연됩니다. defer를 사용하면 JavaScript의 파서 차단은 해결되지만, CSS의 렌더 차단은 여전히 남아 있으므로 Critical CSS 인라인 전략이 중요해집니다.

렌더링 파이프라인과 Core Web Vitals

이제 각 파이프라인 단계가 Core Web Vitals의 어떤 지표에 영향을 미치는지 연결해 봅니다.

파이프라인 단계영향받는 지표영향 방식
HTML 파싱FCP, LCP파싱이 느리면 DOM 구축이 지연되어 첫 콘텐츠 출력이 늦어짐
CSS 파싱 (CSSOM)FCP, LCP렌더 차단 리소스로서 CSSOM 완료 전까지 렌더링 불가
Render TreeFCP, LCPDOM + CSSOM 결합이 완료되어야 렌더링 시작 가능
LayoutINP, CLS강제 Reflow는 INP를 악화시키고, 비동기 레이아웃 변경은 CLS를 유발
PaintINP복잡한 Paint는 프레임 시간을 증가시켜 상호작용 응답성 저하
CompositeINPGPU 가속 활용 시 메인 스레드 부담이 줄어 INP 개선

LCP는 본질적으로 "Render Tree가 완성되고 가장 큰 요소가 Paint되기까지의 시간"입니다. HTML 파싱이 빨라도 CSS가 렌더를 차단하고 있으면 LCP는 개선되지 않습니다. INP는 사용자 상호작용 시 메인 스레드가 얼마나 빨리 응답할 수 있느냐의 문제이므로, Layout Thrashing이나 무거운 Paint가 직접적인 원인이 됩니다. CLS는 Layout 단계의 문제로, 이미지에 크기를 지정하지 않거나 동적 콘텐츠가 삽입되어 기존 요소가 밀릴 때 발생합니다.

정보

이 문서에서 다룬 렌더링 파이프라인이 Core Web Vitals의 기반 지식입니다. 각 지표의 구체적인 측정 기준과 최적화 전략은 Core Web Vitals 가이드에서 상세히 다루고 있습니다.

실전 최적화 패턴

지금까지 배운 파이프라인 지식을 바탕으로, 실무에서 바로 적용할 수 있는 최적화 패턴을 정리합니다.

렌더 차단을 최소화하려면 초기 렌더링에 필요한 최소한의 CSS만 <style> 태그로 <head>에 인라인하고, 나머지 스타일시트는 비동기로 로드합니다.

HTML
<head>
  <!-- Critical CSS: 첫 화면 렌더링에 필요한 최소한의 스타일 -->
  <style>
    body { margin: 0; font-family: system-ui; }
    .hero { height: 100vh; display: flex; align-items: center; }
    .nav { position: fixed; top: 0; width: 100%; }
  </style>
 
  <!-- 나머지 CSS: 비동기 로드 -->
  <link rel="preload" href="full-styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="full-styles.css" /></noscript>
</head>

이 패턴은 CSSOM 구축 시간을 최소화하여 FCP와 LCP를 모두 개선합니다.

정리

핵심 정리

  1. 파싱 단계: HTML은 점진적으로, CSS는 전체가 파싱되어야 렌더링이 시작됩니다. CSS를 <head>에 배치하되 Critical CSS만 인라인하고, JavaScript는 defer로 로드하여 차단을 최소화합니다.

  2. Layout(Reflow)은 파이프라인에서 가장 비싼 연산입니다. DOM 읽기와 쓰기를 분리하여 Layout Thrashing을 방지하고, Layout을 트리거하는 CSS 속성 변경을 최소화합니다.

  3. transform과 opacity만 Composite 단계에서 처리됩니다. 애니메이션은 이 두 속성을 활용하면 메인 스레드를 차단하지 않고 GPU 가속의 이점을 얻을 수 있습니다.

  4. Core Web Vitals와의 연결: LCP는 파싱과 렌더 차단에, INP는 메인 스레드 차단(Layout/Paint)에, CLS는 레이아웃 재계산에 각각 영향을 받습니다.

참고 자료

  • Google Developers - Critical Rendering Path
  • Google Developers - Constructing the Object Model
  • Google Developers - Render-tree Construction, Layout, and Paint
  • Google Developers - Render Blocking CSS
  • MDN - How Browsers Work
  • web.dev - Rendering Performance - CSS 속성별 렌더링 비용 이해
  • Chromium - Life of a Pixel
  • web.dev - Avoid Large, Complex Layouts and Layout Thrashing
Written by

Mirunamu (Park Geonwoo)

Software Developer

관련 글 더보기

Web

브라우저 가비지 컬렉션과 메모리 관리

JavaScript 메모리 구조부터 V8 가비지 컬렉터의 동작 원리, 메모리 누수 디버깅까지

읽기
Web

Core Web Vitals 가이드

Google이 정의한 핵심 사용자 경험 지표(LCP, INP, CLS)와 최적화 전략

읽기
Web

CORS와 Preflight 요청의 동작 원리

Same-Origin Policy부터 Preflight 최적화까지, 브라우저의 교차 출처 요청 제어 메커니즘

읽기
Web

HTTP GET/DELETE 요청에 Body를 넣으면 안 되는 이유

RFC 스펙, 클라이언트/서버 호환성 문제, 그리고 올바른 대안

읽기
이전 글HTTP GET/DELETE 요청에 Body를 넣으면 안 되는 이유
다음 글브라우저 가비지 컬렉션과 메모리 관리
목차
  • 들어가며
  • 렌더링 파이프라인 전체 흐름
  • HTML 파싱과 DOM 트리 구축
  • CSS 파싱과 CSSOM 트리
  • Render Tree 생성
  • Layout (Reflow)
  • Paint와 Paint Order
  • 레이어 합성과 GPU 가속
  • Reflow와 Repaint
  • JavaScript와 렌더 차단
    • async와 defer
    • CSS와 JavaScript의 상호 의존
  • 렌더링 파이프라인과 Core Web Vitals
  • 실전 최적화 패턴
  • 정리
  • 참고 자료