Browser Paint
재도색 (Repaint / Reflow)
React가 DOM을 바꾸면, 브라우저는 앞서 본 렌더링 파이프라인(레이아웃 → 페인트 → 합성)을 변경분에 대해 다시 실행한다.
브라우저 페인트는 React가 커밋으로 DOM을 바꾼 뒤 브라우저가 픽셀을 갱신하기 위해 렌더링 파이프라인을 재실행하는 과정이다. 파이프라인은 JavaScript → 스타일 계산(Style) → 레이아웃(Layout, = 리플로우 reflow) → 페인트(Paint, = 리페인트 repaint) → 합성(Composite)의 다섯 단계로 이루어진다. 변경한 CSS 속성의 성격에 따라 실행되는 단계가 달라지는데, width·height·top 같은 기하 속성을 바꾸면 레이아웃부터 전체가 다시 돈다(리플로우). color·background-image·box-shadow 같은 페인트 전용 속성은 레이아웃을 건너뛰고 페인트→합성만 실행되며, transform·opacity처럼 합성만 필요한 속성은 레이아웃과 페인트를 모두 건너뛰어 가장 저렴하다. 레이아웃 스래싱(layout thrashing)은 JS에서 레이아웃 값을 읽고(offsetHeight 등) 곧바로 쓰기를 반복해 프레임 내에서 강제 동기 레이아웃(forced synchronous layout)을 여러 번 유발하는 안티패턴이다. 브라우저는 이런 비용을 줄이기 위해 여러 DOM 변경을 모아 한 프레임에 배칭(batching)해서 파이프라인을 한 번만 재실행하려 한다.
내부 구성
핵심 포인트
- 렌더링 파이프라인 5단계: JavaScript → Style → Layout(리플로우) → Paint(리페인트) → Composite(합성)
- 리플로우(reflow) = 레이아웃 재계산: 요소의 크기/위치가 바뀌면 발생, 한 요소 변경이 트리 전체에 파급될 수 있어 가장 비쌈
- 리페인트(repaint) = 픽셀 다시 칠하기: 레이아웃은 그대로고 색/배경/그림자 등 시각만 바뀔 때. 레이아웃 단계를 건너뜀
- 합성(composite)만 하는 경로: transform·opacity는 레이아웃·페인트 없이 GPU 레이어만 재합성 → 애니메이션에 이상적
- 리플로우는 반드시 리페인트를 동반하지만, 리페인트가 항상 리플로우를 유발하진 않는다
- 레이아웃 스래싱: 읽기(offsetTop/getComputedStyle)와 쓰기를 번갈아 하면 강제 동기 레이아웃이 반복 발생 → 읽기/쓰기를 분리·배칭해야 한다
- 브라우저는 여러 DOM 변경을 모아 한 프레임에 배칭하여 파이프라인 재실행을 최소화한다
심화
핵심 멘탈 모델은 '아래로 갈수록 싸다'다. 파이프라인은 Style→Layout→Paint→Composite로 갈수록 비용이 줄고, 변경한 속성이 어느 단계에서 시작되느냐가 성능을 결정한다. 그래서 60fps(프레임당 약 16.7ms) 애니메이션의 황금률은 'transform과 opacity만 애니메이트하라'다 — 이 둘은 컴포지터 스레드에서 처리되어 메인 스레드의 레이아웃·페인트를 건너뛰고, 자바스크립트가 바빠도 부드럽게 돈다. will-change나 transform: translateZ(0)로 레이어를 미리 승격시키는 기법도 여기서 나온다(단, 남용하면 메모리·합성 비용 폭증). 면접에서 자주 파고드는 지점은 '강제 동기 레이아웃(레이아웃 스래싱)'이다. 브라우저는 레이아웃 계산을 최대한 미뤄 배칭하지만, JS가 offsetHeight·getBoundingClientRect·getComputedStyle 같은 값을 읽으면 최신 레이아웃을 보장하기 위해 즉시(동기적으로) 레이아웃을 강제한다. 루프 안에서 '한 요소의 높이를 읽고 → 다른 스타일을 쓰고 → 다시 읽고'를 반복하면 매 반복마다 리플로우가 강제되어 O(n) 작업이 O(n^2)처럼 느려진다. 해결책은 FastDOM 패턴처럼 '모든 읽기를 먼저, 모든 쓰기를 나중에' 분리(batch reads then writes)하고, requestAnimationFrame으로 쓰기를 프레임 경계에 맞추는 것이다.
쉽게 말하면 시공(커밋)이 끝나면 다시 청소·페인트(브라우저 재도색)를 해 최종 모습을 완성하는 것.