[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -2,97 +2,214 @@
|
||||
id: wiki-2026-0508-layout-thrashing
|
||||
title: Layout Thrashing
|
||||
category: 10_Wiki/Topics
|
||||
status: needs_review
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: []
|
||||
aliases: [Forced Synchronous Layout, Reflow Thrashing, FSL]
|
||||
duplicate_of: none
|
||||
source_trust_level: A
|
||||
confidence_score: 0.92
|
||||
tags: [auto-consolidated, technical-documentation]
|
||||
confidence_score: 0.9
|
||||
verification_status: applied
|
||||
tags: [web-performance, dom, reflow, browser, javascript]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-08
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
inferred_by: Claude Opus 4.7 (auto-normalize 2026-05-08)
|
||||
tech_stack:
|
||||
language: javascript
|
||||
framework: vanilla/fastdom
|
||||
---
|
||||
|
||||
# [[Layout Thrashing|Layout Thrashing]]
|
||||
# Layout Thrashing
|
||||
|
||||
## 📌 한 줄 통찰 (The Karpathy Summary)
|
||||
레이아웃 스래싱(Layout Thrashing)은 브라우저가 페이지의 구조를 다시 계산해야 하는 리플로우(Reflow)와 리페인트(Repaint)를 과도하게 반복할 때 발생하는 성능 병목 현상입니다 [1, 2]. 주로 자바스크립트가 DOM을 읽고 쓰는 작업을 좁은 루프 안에서 번갈아 수행하거나 레이아웃에 큰 영향을 미치는 CSS 속성을 애니메이션화할 때 유발됩니다 [1, 2]. 이 현상은 프레임 속도를 심각하게 저하시키고 애니메이션을 끊기게 만들어 전반적인 사용자 경험을 훼손합니다 [1, 2].
|
||||
## 매 한 줄
|
||||
> **"매 layout thrashing = read → write → read → write 반복으로 브라우저가 매번 reflow를 강제로 동기 실행하는 안티 패턴"**. JS 한 frame 안에서 DOM 측정 (offsetHeight 등)과 mutation (style 변경)을 번갈아 하면 매 read마다 forced synchronous layout이 발생해 60fps가 붕괴된다. 해결은 read와 write를 batch + rAF로 분리.
|
||||
|
||||
---
|
||||
## 매 핵심
|
||||
|
||||
레이아웃 스래싱(Layout Thrashing)은 스크립트가 DOM을 읽고 쓰는 작업을 짧고 반복적인 루프 내에서 교대로 수행할 때 발생하는 심각한 성능 병목 현상입니다 [1]. 주로 `offsetWidth`나 `offsetHeight` 같은 기하학적 속성을 읽을 때 브라우저가 정확한 크기를 제공하기 위해 내부 레이아웃 큐를 강제로 비우고 동기적 리플로우(Reflow)를 실행하면서 발생합니다 [1]. 이 현상은 브라우저의 렌더링 프레임 속도를 크게 저하시키며, 결과적으로 애니메이션이 끊기거나 웹페이지가 느리게 반응하도록 만듭니다 [1, 2].
|
||||
### 매 왜 발생하나
|
||||
- 브라우저는 효율을 위해 style/layout 계산을 **async + batched**로 처리.
|
||||
- 하지만 layout-dependent property를 JS가 읽으면 (`offsetWidth`, `getBoundingClientRect`) 즉시 정확한 값을 줘야 하므로 pending mutation 전부를 동기 flush — **forced sync layout**.
|
||||
- write → read를 반복하면 매 iteration마다 flush → O(n) reflow.
|
||||
|
||||
## 📖 구조화된 지식 (Synthesized Content)
|
||||
**발생 원인**
|
||||
* **DOM 읽기/쓰기의 반복**: 자바스크립트 스크립트가 좁은 루프 내에서 DOM 속성에 대한 읽기와 쓰기를 번갈아 수행할 때 흔히 발생합니다 [2, 3]. 예를 들어 `offsetWidth`나 `offsetHeight` 같은 크기 관련 속성을 읽을 때, 브라우저는 정확한 치수를 제공하기 위해 내부 레이아웃 큐(Queue)를 강제로 비우고 동기적 리플로우(Synchronous reflow)를 수행해야 하므로 프레임 속도에 악영향을 미칩니다 [2].
|
||||
* **무거운 레이아웃 속성 변경**: `width`, `height`, `margin`, `padding`, `left/top/right/bottom`과 같이 기하학적 형태나 레이아웃에 직접적인 영향을 주는 속성을 애니메이션 처리하면 브라우저가 레이아웃을 지속적으로 재계산하게 되어 레이아웃 스래싱을 유발합니다 [1, 4, 5].
|
||||
### 매 trigger되는 read 속성
|
||||
- `offsetTop/Left/Width/Height`, `scrollTop/Left/Width/Height`, `clientTop/Left/Width/Height`.
|
||||
- `getComputedStyle()`, `getBoundingClientRect()`.
|
||||
- `innerText` (computed style 읽음).
|
||||
- `focus()` (scroll 발생 가능).
|
||||
|
||||
**최적화 및 해결 방법**
|
||||
* **DOM 읽기와 쓰기 분리**: 레이아웃 스래싱을 방지하기 위해서는 DOM 값을 읽는 작업(Read)과 쓰는 작업(Write)을 엄격히 분리하여 수행해야 합니다 [3].
|
||||
* **DOM 변경 일괄 처리([[Batching|Batching]])**: 다수의 DOM 변경을 한 번에 묶어 처리하면 리플로우와 리페인트를 줄일 수 있습니다 [2, 6]. `classList.add()`나 `cssText`를 사용하여 단 한 번의 렌더링 주기만 유발하거나 `innerHTML`을 사용하여 여러 요소를 동시에 업데이트할 수 있습니다 [2, 6].
|
||||
* **가상 DOM 활용**: 새로운 요소를 라이브 DOM에 직접 추가하기 전에 `DocumentFragment`를 활용하여 추가하거나, 노드를 복제하여 변경한 후 원본과 교체하는 방식을 쓰면 요소당 한 번씩 일어날 리플로우를 단 한 번으로 줄일 수 있습니다 [2, 6, 7].
|
||||
* **애니메이션 속성 대체**: 애니메이션을 구현할 때는 레이아웃을 변경하는 속성 대신 GPU 가속의 이점을 얻을 수 있는 `transform`과 `opacity` 속성을 사용하여 리플로우 발생을 피해야 합니다 [5, 8, 9]. 또한 자바스크립트 기반 애니메이션에서는 `requestAnimationFrame`을 사용하여 브라우저의 네이티브 리페인트 주기와 동기화시킴으로써 끊김 없는 모션을 구현해야 합니다 [2, 3, 6].
|
||||
* **스타일 적용 방식 최적화**: 자바스크립트로 직접 인라인 스타일을 조작하기보다는 CSS 클래스를 추가/제거하는 방식을 사용해야 합니다 [6]. 렌더링 속도를 늦추는 깊고 복잡한 CSS 선택자의 사용을 피하고, 자주 변경될 요소에는 `will-change` 속성을 통해 브라우저가 미리 렌더링을 최적화할 수 있는 힌트를 제공하는 것도 방법입니다 [3, 6, 10].
|
||||
### 매 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 안 발생.
|
||||
|
||||
* **발생 메커니즘**
|
||||
* 스크립트가 DOM에 대한 읽기 및 쓰기 작업을 촘촘한 루프 안에서 번갈아 가며 실행할 때 발생합니다 [1].
|
||||
* 스크립트가 요소의 `offsetWidth`나 `offsetHeight` 등을 읽어 들일 때, 브라우저는 정확한 치수를 반환하기 위해 지연된 레이아웃 작업들을 강제로 실행하는 '동기적 리플로우(Synchronous Reflow)'를 유발합니다 [1].
|
||||
* 너비(width), 높이(height), 여백(margin), 위치(left/top/right/bottom) 등 레이아웃에 큰 영향을 미치는 CSS 속성을 애니메이션으로 처리할 때도 브라우저가 레이아웃을 다시 계산하게 되어 레이아웃 스래싱과 리페인트(Repaint) 사이클이 발생할 수 있습니다 [2].
|
||||
* **성능에 미치는 영향**
|
||||
* 브라우저의 초당 프레임 수(FPS)를 급격히 떨어뜨려 성능에 치명적인 영향을 미칩니다 [1].
|
||||
* 특히 모바일이나 저사양 기기에서는 애니메이션이 끊기고(Janky) 버벅거리는 느낌을 주어 사용자 경험을 심각하게 훼손합니다 [2].
|
||||
* **해결 및 방지 기법 (최적화 전략)**
|
||||
* **DOM 읽기/쓰기 분리 및 일괄 처리([[Batching|Batching]])**: DOM에 대한 읽기와 쓰기 작업을 분리하여 레이아웃 스래싱을 방지해야 합니다 [3]. `classList.add()`나 `cssText`를 사용하여 여러 스타일 업데이트를 단일 렌더링 주기로 그룹화(Batch)하는 것이 좋습니다 [1, 4].
|
||||
* **DocumentFragment 사용**: 새로운 요소를 실시간 DOM에 바로 추가하지 않고 `documentFragment`에 먼저 추가한 뒤 라이브 DOM에 한 번에 반영함으로써 리플로우 발생을 요소당 1회로 최소화할 수 있습니다 [1].
|
||||
* **requestAnimationFrame 활용**: [[JavaScript|JavaScript]]로 구동되는 애니메이션이나 DOM 업데이트를 브라우저의 기본 리페인트 주기와 동기화(`requestAnimationFrame` 사용)하여 프레임 드롭이나 스래싱을 방지해야 합니다 [1, 3].
|
||||
* **애니메이션 속성 최적화**: 레이아웃을 변경하는 속성 대신, `transform`이나 `scale`, `opacity`와 같이 리플로우를 유발하지 않고 GPU 가속을 활용할 수 있는 합성(Composite) 단계의 속성만을 사용하여 렌더링 성능을 개선해야 합니다 [2, 5].
|
||||
### 매 응용
|
||||
1. List virtualization.
|
||||
2. Drag & drop.
|
||||
3. Sticky / parallax.
|
||||
4. Animation.
|
||||
5. Resize observer 응답.
|
||||
|
||||
## ⚠️ 모순 및 업데이트 (Contradictions & Updates)
|
||||
No trade-offs available.
|
||||
## 💻 패턴
|
||||
|
||||
## 🔗 지식 연결 (Graph)
|
||||
- **Related Topics:** Reflow, Repaint, [[CSS Animations|CSS Animations]], [[Performance Optimization|Performance Optimization]]
|
||||
- **Projects/Contexts:** [[Frontend|Frontend]] Architecture, [[실무에서 CSS 관리하는 방법|실무에서 CSS 관리하는 방법]]
|
||||
- **Contradictions/Notes:** `will-change` 속성은 브라우저가 변경 사항에 미리 대비하게 해주어 성능을 향상할 수 있지만, 너무 많은 요소에 불필요하게 적용할 경우 오히려 브라우저 자원을 낭비하여 성능 문제를 일으킬 수 있으므로 신중하게 사용해야 합니다 [10].
|
||||
### 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)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
*Last updated: 2026-04-26*
|
||||
### 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';
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- **Related Topics:** [[리플로우(Reflow)|리플로우(Reflow]], 리페인트(Repaint), CSS 성능 최적화(CSS Performance [[Optimization|Optimization]], [[GPU 가속(GPU Acceleration)|GPU 가속(GPU Acceleration]]
|
||||
- **Projects/Contexts:** [[CSS 애니메이션 최적화(Optimizing CSS Animations)|CSS 애니메이션 최적화(Optimizing CSS Animations]], 확장 가능한 프론트엔드 아키텍처(Scalable Frontend [[Architecture|Architecture]]
|
||||
- **Contradictions/Notes:** 소스 전반에서 레이아웃 스래싱을 방지하기 위해 렌더링 파이프라인(Recalculate Style -> Layout -> Paint -> Composite)의 이해가 필수적이라고 강조하며, 성능 저하의 주범으로 레이아웃(리플로우) 단계를 지목하고 있습니다 [1, 5, 6].
|
||||
### 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(); },
|
||||
};
|
||||
|
||||
---
|
||||
*Last updated: 2026-04-26*
|
||||
// usage
|
||||
fastdom.measure(() => {
|
||||
const w = el.offsetWidth;
|
||||
fastdom.mutate(() => { el.style.width = (w * 1.1) + 'px'; });
|
||||
});
|
||||
```
|
||||
|
||||
## 🤖 LLM 활용 힌트 (How to Use This Knowledge)
|
||||
### 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'));
|
||||
```
|
||||
|
||||
**언제 이 지식을 쓰는가:**
|
||||
- *(TODO)*
|
||||
### 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>;
|
||||
}
|
||||
```
|
||||
|
||||
**언제 쓰면 안 되는가:**
|
||||
- *(TODO)*
|
||||
### CSS Containment — reflow 범위 격리
|
||||
```css
|
||||
.card {
|
||||
contain: layout style paint; /* 내부 변화가 외부 reflow 안 일으킴 */
|
||||
}
|
||||
.list-item {
|
||||
contain: layout;
|
||||
content-visibility: auto; /* 보이지 않으면 layout 스킵 */
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 검증 상태 (Validation)
|
||||
### 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" 보라색 막대 확인
|
||||
```
|
||||
|
||||
- **정보 상태:** needs_review
|
||||
- **출처 신뢰도:** A
|
||||
- **검토 이유:** *(P-Reinforce Phase 1 자동 정규화. 본문 검증 필요.)*
|
||||
### 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)`;
|
||||
```
|
||||
|
||||
## 🧬 중복 검사 (Duplicate Check)
|
||||
## 매 결정 기준
|
||||
| 상황 | Approach |
|
||||
|---|---|
|
||||
| 단순 batch | read 먼저 → write 나중 (one pass) |
|
||||
| 여러 모듈이 DOM 만짐 | FastDOM 또는 자체 scheduler |
|
||||
| Animation | rAF + transform/opacity |
|
||||
| 큰 리스트 | virtualization + content-visibility |
|
||||
| 격리된 위젯 | `contain: layout style` |
|
||||
|
||||
- **기존 유사 문서:** *(TODO: 인덱서 클러스터 리포트 참조)*
|
||||
- **처리 방식:** UPDATE (자동 정규화)
|
||||
- **처리 이유:** Phase 1 정규화 — 옛 템플릿/누락 필드 보강.
|
||||
**기본값**: 모든 hot path에서 read/write 분리 + rAF + transform 우선.
|
||||
|
||||
## 🕓 변경 이력 (Changelog)
|
||||
## 🔗 Graph
|
||||
- 부모: [[Browser-Rendering-Pipeline]], [[Web-Performance]]
|
||||
- 변형: [[Forced-Synchronous-Layout]], [[Reflow]], [[Repaint]]
|
||||
- 응용: [[Virtual-Scrolling]], [[Drag-and-Drop]], [[Sticky-Header]]
|
||||
- Adjacent: [[CSS-Containment]], [[requestAnimationFrame]], [[ResizeObserver]], [[Core-Web-Vitals]]
|
||||
|
||||
| 날짜 | 변경 내용 | 처리 방식 | 신뢰도 |
|
||||
|------|-----------|-----------|--------|
|
||||
| 2026-05-08 | P-Reinforce Phase 1 정규화 (frontmatter + 헤더 표준화) | UPDATE | A |
|
||||
## 🤖 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 추가 |
|
||||
|
||||
Reference in New Issue
Block a user