Reflow e repaint — medir e eliminar
TL;DR
Reflow (layout) é o custo de recalcular geometria — caro porque cascatea. Repaint é redesenhar pixels sem recalcular posições — barato mas frequente. Layout thrashing (alternar leituras e escritas de propriedades de layout) força reflows múltiplos num único frame. A solução é batch: ler tudo, depois escrever tudo. O DevTools Performance aba é a ferramenta para identificar e medir.
O que aciona reflow vs repaint vs composite
flowchart TD CHANGE["Mudança de estilo/DOM"] CHANGE --> Q1{"Afeta geometria\nou posição?"} Q1 -->|sim| REFLOW["Reflow (Layout)\n↓\nRepaint\n↓\nComposite"] Q1 -->|não| Q2{"Afeta visual\nsem geometria?"} Q2 -->|sim| REPAINT["Repaint apenas\n↓\nComposite"] Q2 -->|não| COMPOSITE["Composite apenas\n(transform, opacity)"]
Propriedades que causam reflow (lista principal)
/* Dimensões */
width, height, min-width, max-width, min-height, max-height
/* Espaçamento */
margin, padding, border-width, border-style
/* Posição */
top, right, bottom, left (com position)
position (mudança de static para absolute, etc.)
/* Texto */
font-size, font-family, font-weight, line-height
white-space, word-break
/* Conteúdo */
/* Inserir/remover elementos */
/* Mudar textContent */
/* Visual que afeta layout */
display (none ↔ block/flex/etc.)
overflow (pode afetar scrollbars)Propriedades que causam apenas repaint
color, background-color, background-image
box-shadow, text-shadow, outline
border-color (sem mudar width)
visibility (hidden ↔ visible)Propriedades que vão direto para composite
transform /* translate, rotate, scale, skew */
opacity
filter /* blur, etc. — varia por browser */Layout thrashing — o anti-pattern clássico
Thrashing acontece quando você lê e escreve propriedades de layout alternadamente:
// ❌ Thrashing — O(N) reflows
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
// Leitura — o browser precisa fazer o reflow para ter o valor atualizado
const width = box.offsetWidth;
// Escrita — invalida o layout calculado
box.style.width = (width * 1.1) + 'px';
// Próxima leitura → outro reflow forçado
});// ✅ Batch reads → batch writes — O(1) reflow total
const boxes = [...document.querySelectorAll('.box')];
// Fase 1: todas as leituras
const widths = boxes.map(box => box.offsetWidth);
// Fase 2: todas as escritas
boxes.forEach((box, i) => {
box.style.width = (widths[i] * 1.1) + 'px';
});Leituras que forçam layout sync (forced synchronous layout)
// Essas propriedades, quando lidas após uma escrita, forçam reflow imediato:
// Geometry
offsetWidth, offsetHeight, offsetTop, offsetLeft, offsetParent
clientWidth, clientHeight, clientTop, clientLeft
scrollWidth, scrollHeight, scrollTop, scrollLeft
// Position / Size
getBoundingClientRect()
getClientRects()
// Computed style
getComputedStyle(el).width // qualquer dimensão
getComputedStyle(el).transform // valores calculados
// Scroll
window.scrollX, window.scrollY
element.scrollIntoView()
// Focus (pode afetar layout ao exibir outline)
element.focus()Medir com DevTools Performance
1. Abrir DevTools → Performance
2. Clicar no botão de gravação
3. Interagir com a página
4. Parar a gravação
5. Procurar:
- "Layout" em amarelo (reflow custoso)
- "Paint" em verde
- "Composite Layers" em cinza (ok — barato)
- Barras vermelhas no topo = frames perdidos (> 16ms)
Para identificar thrashing especificamente:
- Procure por “Forced reflow is a likely performance bottleneck” no DevTools console
- Ou use a API de performance:
// Medir tempo de operação
performance.mark('start-render');
renderList(data);
performance.mark('end-render');
performance.measure('render', 'start-render', 'end-render');
const measure = performance.getEntriesByName('render')[0];
console.log(`Render: ${measure.duration.toFixed(2)}ms`);FastDOM — pattern de batch automático
Para código legado com muitas leituras e escritas espalhadas, a biblioteca FastDOM batcheia automaticamente:
import fastdom from 'fastdom';
function updateElement(el) {
// ✅ FastDOM garante que todas as leituras ficam juntas e todas as escritas juntas
fastdom.measure(() => {
const height = el.offsetHeight;
fastdom.mutate(() => {
el.style.height = (height + 10) + 'px';
});
});
}
// Pode ser chamado muitas vezes — FastDOM batcheia no próximo frame
elements.forEach(updateElement);Padrões para evitar thrashing
1. Ler tudo antes de escrever
// ✅ Ler, depois escrever
function syncHeights(source, targets) {
const height = source.offsetHeight; // UMA leitura
targets.forEach(el => {
el.style.height = height + 'px'; // múltiplas escritas OK
});
}2. Cache de leituras quando possível
// ✅ Cachear dimensões que não mudam no loop
const containerWidth = container.offsetWidth; // uma leitura
items.forEach(item => {
item.style.width = (containerWidth / items.length) + 'px';
});3. requestAnimationFrame para separar leituras de escritas por frame
function readThenWrite() {
// Frame atual: ler
const height = el.offsetHeight;
// Próximo frame: escrever (depois que o browser atualizou o layout)
requestAnimationFrame(() => {
el.style.height = (height + 10) + 'px';
});
}4. CSS Variables para evitar leituras de JS
// ❌ Ler width para calcular posição
const width = container.offsetWidth;
item.style.left = (width / 2) + 'px';
// ✅ CSS variáveis — o browser cuida do cálculo
container.style.setProperty('--container-w', container.offsetWidth + 'px');.item {
left: calc(var(--container-w) / 2);
}ResizeObserver — alternativa a ouvir resize
Ouvir window.resize para reagir a mudanças de tamanho é ineficiente — dispara muito e pode causar thrashing:
// ❌ window.resize — caro
window.addEventListener('resize', () => {
const w = container.offsetWidth; // leitura
adjustLayout(w); // escrita
});
// ✅ ResizeObserver — entregue após layout, não causa thrashing
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect; // disponível sem leitura adicional
adjustLayout(width);
}
});
observer.observe(container);Para fixar
- O que é layout thrashing? Escreva um exemplo concreto com um loop que causa thrashing e a versão corrigida.
getComputedStyle(el).widthapós uma mudança de CSS: causa reflow? Por quê?- No DevTools Performance, o que indica um frame com layout thrashing? Onde você vê?
- Por que usar
ResizeObserverem vez de ouvirwindow.resizepara ajustar layout responsivo?- O que FastDOM faz? Como ele resolve thrashing sem que o desenvolvedor precise gerenciar manualmente a ordem das operações?
Veja também
- 03 — Compositing — anterior
- 05 — CRP otimizado — próxima
- DOM 06 — DocumentFragment — batch mutations para evitar reflows