Browswer Rendering
먼저 reflow를 알기 위해서는 브라우저 렌더링 과정을 이해해야 한다.

- HTML 파싱
- 브라우저는 HTML 문서를 받아서 읽고 DOM(Document Object Model) 트리를 생성한다.
- CSS 처리
- 모든 CSS를 파싱하고 CSSOM(CSS Object Model) 트리를 생성한다.
- 렌더 트리 생성
- DOM과 CSSOM을 결합하여 렌더 트리를 생성한다. 실제로 화면에 렌더링될 요소들만 포함된다.
- 레이아웃 계산
- 요소들의 정확한 위치와 크기를 계산한다. viewport 내에서 요소들의 정확한 위치를 결정한다.
- 이 과정을 리플로우
reflow라고도 부른다.
- 페인트
- 렌더 트리의 각 노드를 실제 픽셀로 변환하고 색상, 테두리, 그림자 등 모든 시각적 스타일을 그린다.
- 컴포지션
- 페인트된 레이어들을 최종적으로 합성하여 최종 이미지를 만들어 화면에 표시한다.
Reflow
reflow는 브라우저가 페이지의 레이아웃을 다시 계산하는 과정이다.DOM 요소의 기하학적 속성(크기, 위치, 레이아웃)이나 CSS 스타일이 변경되면, 브라우저는 각 요소가 화면에 어떻게 배치될지 다시 계산해야 한다.
이 과정은 모든 자식 요소와 관련된 부모 요소까지 영향을 주기 때문에 비용이 매우 높은 작업이다.
또한
reflow는 CPU를 사용한다.브라우저의 크기가 변경되면 이것도 결국 다시 reflow가 발생하는 것일까?
reflow를 주로 발생시키는 작업은 다음과 같다.- DOM 요소 추가/삭제
- 요소의 크기 변경 (
width,height)
- 위치 변경 (
margin,padding)
- 브라우저 창 크기 조정
- 폰트 변경
- 텍스트 내용 변경
- CSS 속성 중 레이아웃에 영향을 주는 속성 변경 (
display,position)
- 레이아웃 정보 접근
- element.offsetWidth, getComputedStyle() 같은 속성 접근
- 브라우저는 최신 값을 제공하기 위해 reflow를 강제로 수행
위 작업들은 개발하면서 자주 사용하는 작업이고 모두
reflow를 발생시킨다.Repaint
repaint는 요소의 외관이 변경되지만 레이아웃에는 영향을 주지 않을 때 발생한다. 요소의 시각적 스타일(색상, 배경)만 다시 그리는 과정으로, 브라우저는 요소의 모양만 다시 그리면 되기 때문에
reflow보다는 연산 비용이 상대적으로 낮다.하지만 여전히 성능에는 영향을 줄 수 있다.
repaint 는 GPU를 사용한다.repaint를 주로 발생시키는 작업은 다음과 같다.background-color변경
color변경
visibility변경
box-shadow변경
outline변경
위 작업들은 개발하면서 자주 사용되는 작업이고 모두
repaint를 발생시킨다.Reflow 최적화하기
Reflow 최적화하는 방법들은 코드와 함께 알아보겠다.
기존 코드
import React, { useEffect, useRef } from 'react'; export default function ReflowBad() { const containerRef = useRef(null); useEffect(() => { const container = containerRef.current; const children = container.children; const start = performance.now(); for (let i = 0; i < children.length; i++) { const height = children[i].offsetHeight; // 💥 이 시점마다 reflow 발생 children[i].style.marginTop = `${height / 10}px`; } const end = performance.now(); console.log(`⛔️ 비최적화 처리 시간: ${end - start}ms`); }, []); return ( <div ref={containerRef}> {Array.from({ length: 1000 }, (_, i) => ( <div key={i} style={{ height: 20, backgroundColor: '#ccc', margin: 2 }}> Item {i} </div> ))} </div> ); }
브라우저는 똑똑하다.
여러 레이아웃 정보 접근이 연속해서 일어나면, 내부적으로 “레이아웃 캐시”를 사용해서 한 번만 계산한다.
그러나 위 코드는 읽기 후 쓰게 되면 다시 최신 브라우저가 필요하므로 루프마다 reflow가 일어나는 것이다.
결과는 아래 이미지에서처럼 약 9.2ms만큼 소요되었다.

Batch 처리
레이아웃 측정과 DOM 업데이트를 분리하여 브라우저가 중간에 리플로우 하지 않도록 유도하는 방식이다.
import React, { useEffect, useRef } from 'react'; export default function ReflowGood() { const containerRef = useRef(null); useEffect(() => { const container = containerRef.current; const children = container.children; const start = performance.now(); // ✅ Reflow 최소화를 위해 layout 측정 먼저 끝낸 후 스타일 변경 const heights = Array.from(children).map((el) => el.offsetHeight); for (let i = 0; i < children.length; i++) { children[i].style.marginTop = `${heights[i] / 10}px`; } const end = performance.now(); console.log(`✅ 최적화 처리 시간: ${end - start}ms`); }, []); return ( <div ref={containerRef}> {Array.from({ length: 1000 }, (_, i) => ( <div key={i} style={{ height: 20, backgroundColor: '#ccc', margin: 2 }}> Item {i} </div> ))} </div> ); }
Batch처리를 하여 reflow가 1번만 발생하도록 하였다.
결과는 1.3초다..!

최적화를 하지 않은 상태보다 속도가 약 7배나 향상되었다…!!
실무에서는 정말 유의미한 향상일 것이다.
실제로 내 프로젝트 ‘PrayU’에서는 초기 한 화면에 모든 컴포넌트들이 있어서,
shadcn의 드로워나 카드들을 넘길 때 뚝뚝 끊기는 현상도 자주 발견되었다.
그 때는 데이터 fetch의 속도를 줄이고, fetch의 시점을 다르게 분리하면서 지연을 줄이려 노력했다.
이렇게 reflow의 개념을 알았다면 좀 더 향상시킬 수 있었을 것 같다.
className을 사용한 스타일 일괄 변경
// ❌ Bad (여러 style 직접 수정 → reflow 유발) el.style.width = '100px'; el.style.height = '100px'; el.style.marginTop = '20px'; // ✅ Good (class 하나로 전체 적용 → 1번만 reflow) el.classList.add('updated-layout');
DOM Depth 최소화
깊은 트리 구조는 reflow의 전파가 넓게 퍼진다.
- 가능하면 중첩 DOM 구조를 단순화하고
- 한 요소의 위치나 크기가 바뀔 때 부모/형제까지 영향받는 구조 지양하자
will-change, transform, opacity 사용
CSS에서 layout 영향을 안 주는 transform, opacity 등을 사용하면 Reflow가 아닌 Repaint 또는 GPU 가속 처리로 최적화 가능하다.
// ✅ 애니메이션에 적합 (layout 영향 없음) .element { transition: transform 0.3s ease; will-change: transform; } // ❌ layout을 변경하므로 reflow 유발 .element { transition: top 0.3s ease; }
Throttling, Debouncing에서 조심하기
프레임마다 변화시키지 않기
- resize, scroll, input 등 이벤트는 매우 자주 발생
- 그 안에서 DOM 측정/변경을 하지 말고, requestAnimationFrame 또는 lodash.debounce 사용
// ✅ scroll에 reflow 유발 코드 직접 쓰지 말고 window.addEventListener('scroll', () => { window.requestAnimationFrame(() => { const height = myEl.offsetHeight; ... }); });
Virtualization
대량의 DOM 노드를 직접 렌더링하지 않고, 보이는 범위만 렌더링한다.
react-window랑 @tanstack/react-virtual 라이브러리가 있는데,동적으로 구현하기 위해서
@tanstack/react-virtual 를 주로 사용하는 것 같다.주로 대형 리스트를 최적화할 때 사용한다.

요즘모든 SNS 에 필수적으로 들어가는 기능인 무한 스크롤(Infinite scroll)은 웹 페이지에서 사용자가 스크롤을 내릴 때 자동으로 추가 콘텐츠를 로드하여 페이지를 끊임없이 확장하는 기술로, 스크롤을 끝까지 내리면 새로운 콘텐츠가 동적으로 로드되어 사용자 경험을 향상시킨다.
또한 페이지 이동 없이 사용자가 머무는 시간을 늘려 사용자 유지율을 향상시킬 수 있어 웹 어플리케이션에 자주 사용되는 기술 이다.
하지만 페이지네이션과 다르게 무한 스크롤 기법은 이름답게 데이터만 충분하다면 DOM에도 무한한 요소들이 생성될 수 있다. 이는 곧 성능 저하를 유발하고, UX에 악영향을 미치게 된다.

Vitrualization 기법을 통해 브라우저 뷰포트를 벗어난 부분의 요소는 랜더링하지 않아서 브라우저의 부담을 줄일 수 있다.
참고



