---
id: wiki-2026-0508-layout-thrashing
title: Layout Thrashing
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [Forced Synchronous Layout, Reflow Thrashing, FSL]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [web-performance, dom, reflow, browser, javascript]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: javascript
  framework: vanilla/fastdom
---

# Layout Thrashing

## 매 한 줄
> **"매 layout thrashing = read → write → read → write 반복으로 브라우저가 매번 reflow를 강제로 동기 실행하는 안티 패턴"**. JS 한 frame 안에서 DOM 측정 (offsetHeight 등)과 mutation (style 변경)을 번갈아 하면 매 read마다 forced synchronous layout이 발생해 60fps가 붕괴된다. 해결은 read와 write를 batch + rAF로 분리.

## 매 핵심

### 매 왜 발생하나
- 브라우저는 효율을 위해 style/layout 계산을 **async + batched**로 처리.
- 하지만 layout-dependent property를 JS가 읽으면 (`offsetWidth`, `getBoundingClientRect`) 즉시 정확한 값을 줘야 하므로 pending mutation 전부를 동기 flush — **forced sync layout**.
- write → read를 반복하면 매 iteration마다 flush → O(n) reflow.

### 매 trigger되는 read 속성
- `offsetTop/Left/Width/Height`, `scrollTop/Left/Width/Height`, `clientTop/Left/Width/Height`.
- `getComputedStyle()`, `getBoundingClientRect()`.
- `innerText` (computed style 읽음).
- `focus()` (scroll 발생 가능).

### 매 trigger되는 write
- 어떤 layout 영향 style 변경: `el.style.width`, class 변경, DOM insert/remove.
- `setAttribute` (size/position 영향).

### 매 해결 원칙
1. **Read 모두 먼저**, 그다음 **write 모두**.
2. **rAF**: 측정은 현재 frame, 변경은 다음 frame.
3. **FastDOM** 같은 scheduler.
4. **CSS Containment** (`contain: layout`) — reflow 범위 격리.
5. **Transform/opacity** — composite-only, layout 안 발생.

### 매 응용
1. List virtualization.
2. Drag & drop.
3. Sticky / parallax.
4. Animation.
5. Resize observer 응답.

## 💻 패턴

### Anti-pattern — read/write 인터리브
```js
// ❌ N번 forced sync layout
const items = document.querySelectorAll('.item');
items.forEach(el => {
  const w = el.offsetWidth;          // read (flush!)
  el.style.width = (w * 1.1) + 'px'; // write (invalidate)
});
```

### Fix — batch read 후 batch write
```js
// ✅ 1번만 layout 계산
const items = document.querySelectorAll('.item');
const widths = [];
for (const el of items) widths.push(el.offsetWidth);   // all reads
for (let i = 0; i < items.length; i++) {
  items[i].style.width = (widths[i] * 1.1) + 'px';     // all writes
}
```

### requestAnimationFrame + double-buffer
```js
function resizeAll() {
  const items = [...document.querySelectorAll('.item')];
  // measure phase
  const widths = items.map(el => el.offsetWidth);
  // mutate phase — 다음 frame
  requestAnimationFrame(() => {
    items.forEach((el, i) => {
      el.style.width = (widths[i] * 1.1) + 'px';
    });
  });
}
```

### FastDOM 스타일 scheduler
```js
const reads = [];
const writes = [];
let scheduled = false;
function schedule() {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(() => {
    while (reads.length)  reads.shift()();
    while (writes.length) writes.shift()();
    scheduled = false;
  });
}
export const fastdom = {
  measure(fn) { reads.push(fn);  schedule(); },
  mutate(fn)  { writes.push(fn); schedule(); },
};

// usage
fastdom.measure(() => {
  const w = el.offsetWidth;
  fastdom.mutate(() => { el.style.width = (w * 1.1) + 'px'; });
});
```

### ResizeObserver — read/write 분리
```js
const ro = new ResizeObserver(entries => {
  // entries already has measurements — read 안 해도 됨
  const updates = entries.map(e => ({
    el: e.target,
    height: e.contentRect.height,
  }));
  requestAnimationFrame(() => {
    for (const u of updates) u.el.style.setProperty('--h', u.height + 'px');
  });
});
ro.observe(document.querySelector('#panel'));
```

### React — useLayoutEffect로 측정·변경 분리
```tsx
import { useLayoutEffect, useRef, useState } from "react";
function AutoFit({ children }) {
  const ref = useRef<HTMLDivElement>(null);
  const [w, setW] = useState(0);
  useLayoutEffect(() => {
    // 측정만
    const next = ref.current!.offsetWidth;
    setW(next);  // 변경은 React commit이 batch
  }, []);
  return <div ref={ref} style={{ minWidth: w }}>{children}</div>;
}
```

### CSS Containment — reflow 범위 격리
```css
.card {
  contain: layout style paint;   /* 내부 변화가 외부 reflow 안 일으킴 */
}
.list-item {
  contain: layout;
  content-visibility: auto;       /* 보이지 않으면 layout 스킵 */
}
```

### Performance 측정 — DevTools Performance API
```js
performance.mark('measure-start');
const w = el.offsetWidth;
performance.mark('measure-end');
performance.measure('forced-layout', 'measure-start', 'measure-end');
// DevTools Performance tab에서 "Recalculate Style" / "Layout" 보라색 막대 확인
```

### Transform 사용으로 layout 회피
```js
// ❌ left/top — layout 발생
el.style.left = x + 'px'; el.style.top = y + 'px';
// ✅ transform — composite-only
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| 단순 batch | read 먼저 → write 나중 (one pass) |
| 여러 모듈이 DOM 만짐 | FastDOM 또는 자체 scheduler |
| Animation | rAF + transform/opacity |
| 큰 리스트 | virtualization + content-visibility |
| 격리된 위젯 | `contain: layout style` |

**기본값**: 모든 hot path에서 read/write 분리 + rAF + transform 우선.

## 🔗 Graph
- 부모: [[Web-Performance]]
- 변형: [[Forced-Synchronous-Layout]], [[Reflow]], [[Repaint]]
- 응용: [[Drag-and-Drop]]
- Adjacent: [[requestAnimationFrame]], [[ResizeObserver]], [[Core Web Vitals Optimization (INP, LCP 개선)|Core-Web-Vitals]]

## 🤖 LLM 활용
**언제**: 코드에서 read/write interleave 탐지, FastDOM-style 리팩터 제안, Performance trace 해석.
**언제 X**: 실제 frame budget 측정 — 디바이스/페이지 의존, DevTools 직접.

## ❌ 안티패턴
- **루프 안에서 offsetX 호출**: 가장 흔한 thrashing.
- **jQuery `.css()` 연쇄**: 내부에서 measure 트리거.
- **window.scroll listener에서 read+write**: scroll 매 이벤트마다 layout.
- **MutationObserver 콜백에서 sync read**: 배치 의미 사라짐.
- **Transform 가능한데 left/top 사용**: 불필요한 layout.

## 🧪 검증 / 중복
- Verified (Chrome DevTools docs, web.dev "Avoid large, complex layouts" 2026, Wilson Page FastDOM).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — FastDOM, useLayoutEffect, CSS containment 추가 |
