docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
---
|
||||
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, CLS)|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 추가 |
|
||||
Reference in New Issue
Block a user