Reflow는 왜 느릴까
"레이아웃 계산이 무겁다"는 말은 자주 듣지만, 정확히 무엇이 느린 건지는 은근히 설명하기 어려워요. 렌더링 파이프라인을 따라가면서 Reflow가 왜 다른 단계보다 비싼지, 그리고 왜 코드 한 줄이 수십 개의 요소를 다시 계산하게 만드는지 짚어볼게요.
렌더링 파이프라인 먼저
브라우저는 DOM이 바뀔 때마다 5단계를 거쳐 화면을 다시 그려요. 이 중 어디서부터 다시 실행하느냐가 성능을 가릅니다.
transform이나 opacity만 바꾸면
스타일 재계산과 페인트까지는 건드리지 않고 합성 단계에서 바로 처리돼요.
반대로 width, top,
font-size처럼 기하 정보를 바꾸는 속성은 반드시
레이아웃부터 다시 타고 내려와야 해요. 문제는 이 레이아웃 단계가 유독 무겁다는
거예요.
왜 하필 레이아웃이 비쌀까
핵심은 이거예요. 레이아웃은 요소 하나만 독립적으로 계산할 수 없는 문제예요.
페인트는 "이 픽셀에 이 색을 칠해라"처럼 지역적인 작업이지만, 레이아웃은 "이 요소가 화면 어디에, 얼마나 크게 있어야 하는가"를 계산해요. 그런데 한 요소의 크기와 위치는 부모의 너비, 형제 요소들이 차지한 공간, 자식 콘텐츠의 양에 따라 달라져요. 즉 기하 정보는 트리 전체에 걸쳐 서로 얽혀있는 제약 충족 문제에 가까워요.
요소 2 하나의 높이가 늘어났을 뿐인데, 그 아래 있는 요소 3, 4는 전부 새로운 위치를 다시 계산해야 해요. 자식이 많고 트리가 깊을수록 이 파급 효과는 더 넓게 퍼지고, 최악의 경우 문서 전체를 다시 계산해야 하는 상황(전역 reflow)까지 이어져요.
Layout Thrashing — 스스로 발목 잡기
진짜 성능 문제는 대부분 여기서 시작해요. 브라우저는 원래 여러 개의 DOM 변경을 모아뒀다가 한 번에 레이아웃을 계산하려고 해요(배치 처리). 그런데 자바스크립트가 DOM을 쓴 직후에 곧바로 크기·위치 값을 읽으면, 브라우저는 정확한 값을 주기 위해 예정에 없던 레이아웃을 즉시 강제로 실행해요. 이걸 forced synchronous layout, 반복되면 layout thrashing이라고 불러요.
// 매 반복마다 강제 레이아웃 발생 boxes.forEach(box => { const h = box.offsetHeight // 읽기 → 레이아웃 강제 실행 box.style.height = (h + 10) + 'px' // 쓰기 → 다음 읽기 전까지 대기중 })
// 모든 값을 먼저 읽고, 그 다음에 전부 쓴다 const heights = boxes.map(box => box.offsetHeight) boxes.forEach((box, i) => { box.style.height = (heights[i] + 10) + 'px' })
읽기를 전부 끝낸 다음 쓰기를 모아서 실행하면, 브라우저는 레이아웃을 딱 한 번만 계산하면 돼요. 100개 요소를 반복문 안에서 읽고 쓰면 최악의 경우 100번의 강제 레이아웃이 발생할 수 있어요.
offsetTop/Left/Width/Height,
clientWidth/Height,
scrollTop/Height,
getComputedStyle(),
getBoundingClientRect() — 이런 값을 DOM 쓰기 직후에
읽으면 브라우저가 대기 중이던 변경 사항을 즉시 반영해서 값을 계산해줘요.
속성별 비용 — 뭘 바꾸느냐가 다르다
| 단계 | 대표 속성 | 비용 |
|---|---|---|
| Layout부터 | width, height, top, left, font-size, padding | 비쌈 |
| Paint부터 | color, background, box-shadow, border-radius | 중간 |
| Composite만 | transform, opacity | 저렴 |
그래서 애니메이션을 만들 때는 left를 바꾸는 대신
transform: translateX()를 쓰라는 말이 나오는 거예요.
결과는 비슷해 보여도, 하나는 매 프레임마다 레이아웃을 다시 계산하고 다른 하나는
GPU가 레이어만 이동시켜요.
정리하면
- 레이아웃은 요소 하나만의 문제가 아니라 부모·형제·자식이 얽힌 전역 계산이라 비싸다
- 한 요소의 크기 변화는 그 아래 형제들의 위치까지 연쇄적으로 다시 계산하게 만든다
- DOM 쓰기 직후 크기·위치를 읽으면 예정된 배치 처리를 깨고 강제 레이아웃을 유발한다
- 애니메이션은 가능하면 transform·opacity로, 읽기와 쓰기는 반드시 분리해서 처리한다
'Frontend' 카테고리의 다른 글
| [Frontend] 콜백 지옥에서 벗어나기 (0) | 2026.07.03 |
|---|---|
| [Frontend] React Hook - 함수 컴포넌트 이해하기 (0) | 2026.07.03 |
| [Frontend] npm, Yarn, pnpm 정리 (0) | 2026.07.03 |
| [Frontend] Redux와 Recoil의 차이 (0) | 2026.07.03 |
| [Frontend] FLOW-3D 교량 세굴 3D 대시보드 (0) | 2026.07.02 |