# Paint profiling clean; no layout storms

- **Pattern:** `ab-002049` (`performance-deep-dive.regression-prevention.paint-profiling-clean`)
- **Severity:** info
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002049
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002049)

## Why it matters

Layout thrashing occurs when JavaScript alternates between DOM reads (which force the browser to recalculate layout) and DOM writes within the same synchronous block. A loop that reads `el.offsetHeight` then writes `el.style.height` for 50 elements triggers 50 layout recalculations instead of one. ISO 25010:2011 time-behaviour measures the cost; the user-visible outcome is continuous paint flashing and dropped frames during any interaction that touches DOM geometry — typically resizing, drag, and scroll handlers.

## Severity rationale

Info because layout thrashing produces cosmetic jank and is difficult to trigger in synthetic tests, making it a polish-level issue rather than a blocking defect — though it can be severe on devices with constrained GPU memory.

## Remediation

Separate all DOM reads from DOM writes by batching writes into a `requestAnimationFrame` callback. Read geometry values before the frame, apply changes inside the callback.

```ts
// components/resize-handler.ts — avoid layout thrashing
function resizeElements(elements: HTMLElement[]) {
  // Phase 1: read all geometry (single layout pass)
  const heights = elements.map((el) => el.offsetHeight)

  // Phase 2: write all geometry in next frame (single repaint)
  requestAnimationFrame(() => {
    elements.forEach((el, i) => {
      el.style.height = heights[i] + 10 + 'px'
    })
  })
}
```

## Detection

- **ID:** `paint-profiling-clean`
- **Severity:** `info`
- **What to look for:** Count all DOM manipulation patterns. Enumerate locations where DOM reads and writes are interleaved (potential layout thrashing). Use Chrome DevTools → Rendering tab. Enable "Paint flashing" and interact with the app. Look for excessive red squares indicating repaints. Check for "layout thrashing" patterns (rapid reads and writes to DOM).
- **Pass criteria:** Paint profiling shows no more than 5 repaint events per user interaction. No layout storms or thrashing detected. DOM reads and writes are batched. Zero layout thrashing patterns detected (interleaved read-write sequences in loops).
- **Fail criteria:** Frequent repaints indicated by continuous red flashing. Layout thrashing detected (read, write, read, write pattern).
- **Skip (N/A) when:** Never — paint profiling applies to all applications.

- **Cross-reference:** For GC pause impact on paint, see `gc-pauses-under-50ms`.
- **Detail on fail:** `"Continuous paint flashing during scroll — indicates excessive repaints and poor performance"` or `"Layout thrashing detected: DOM reads and writes interleaved in tight loop"`
- **Remediation:** Batch DOM operations to avoid layout thrashing.

  ```tsx
  // Batch DOM operations in components/resize-handler.ts
  const height = el.offsetHeight  // read
  requestAnimationFrame(() => { el.style.height = height + 10 + 'px' })  // write in next frame
  ```

## External references

- iso-25010 time-behaviour

Taxons: performance

HTML version: https://auditbuffet.com/patterns/ab-002049
