Files
2nd/10_Wiki/Topics/AI_and_ML/Layout_Thrashing.md
T
koriweb d8a80f6272 chore(wiki): dangling 링크 canonical 정규화 (768파일/1200건)
이름만 다른(표기 변형) [[위키링크]]를 대상 문서의 canonical 제목으로 치환해
끊겼던 1,200개 링크를 연결. 제목/파일명 정규화 일치만 적용하고 별칭 매칭은
과병합 위험으로 제외(애매성 가드). 원본은 _link_reconcile_backup/ 에 백업.
도구: Datacollect/scripts/link_reconcile_apply.mjs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:24:15 +09:00

6.8 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-layout-thrashing Layout Thrashing 10_Wiki/Topics verified self
Forced Synchronous Layout
Reflow Thrashing
FSL
none A 0.9 applied
web-performance
dom
reflow
browser
javascript
2026-05-10 pending
language framework
javascript 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 인터리브

// ❌ 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

// ✅ 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

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

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 분리

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로 측정·변경 분리

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 범위 격리

.card {
  contain: layout style paint;   /* 내부 변화가 외부 reflow 안 일으킴 */
}
.list-item {
  contain: layout;
  content-visibility: auto;       /* 보이지 않으면 layout 스킵 */
}

Performance 측정 — DevTools Performance API

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 회피

// ❌ 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

🤖 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 추가