wandres.dev
RENDERIZADO II · Layout thrashing y el DOM

La lista completa: todo lo que fuerza layout o recálculo de estilo

El inventario exhaustivo de propiedades, métodos y APIs que obligan al navegador a resolver estilo o layout de forma síncrona, agrupado por familia y con lo que fuerza cada uno.

⏱ 20 min

Todo el mundo sabe que offsetHeight fuerza layout. Casi nadie sabe que innerText también, que elementFromPoint también, que la mitad de la API de texto de SVG también, y que getComputedStyle depende de qué propiedad le pidas. Esta es la lista reunida, agrupada por familia, distinguiendo lo que fuerza el layout completo de lo que solo fuerza el recálculo de estilo. Es material de consulta: no se memoriza, se tiene a mano.

🎯 Al terminar esta lección sabrás
  • Consultar, para cualquier API del DOM, si fuerza layout, si fuerza estilo o si no fuerza nada.
  • Distinguir el coste del recálculo de estilo del coste del layout.
  • Reconocer las familias completas que un desarrollador medio no asocia con geometría: texto, SVG, eventos y accesibilidad.
  • Identificar las APIs equivalentes que no fuerzan y sirven de sustituto.

Geometría de la caja: la familia obvia

Estas son las que todo el mundo conoce, y son las que más aparecen. Todas fuerzan layout si hay invalidación pendiente.

API Nota
elem.offsetWidth, elem.offsetHeight Enteros redondeados, incluyen borde
elem.offsetTop, elem.offsetLeft Relativos a offsetParent
elem.offsetParent La búsqueda del ancestro posicionado requiere layout
elem.clientWidth, elem.clientHeight Sin borde ni barra de scroll
elem.clientTop, elem.clientLeft Grosor del borde superior e izquierdo
elem.getBoundingClientRect() Devuelve decimales, no enteros; incluye transformaciones
elem.getClientRects() Una caja por línea en contenido en línea

La diferencia entre offsetWidth y getBoundingClientRect().width no es cosmética: la primera devuelve un entero e ignora la escala de las transformaciones; la segunda devuelve un float y sí aplica la matriz de transformación acumulada. Si estás animando con transform y mides con offsetWidth, mides el elemento sin transformar.

En el document y en documentElement, clientWidth y clientHeight son el tamaño del viewport sin barras de scroll: la lectura clásica para saber cuánto espacio hay. Fuerza layout igual que cualquier otra.

Scroll, texto y accesibilidad: las que nadie espera

Aquí es donde la lista deja de ser evidente. Todas las de esta tabla fuerzan layout.

API Por qué
elem.scrollTop, elem.scrollLeft Al leer y al escribir
elem.scrollWidth, elem.scrollHeight Dimensión del contenido desbordado
elem.scrollTo(), elem.scrollBy() Necesitan la geometría de destino
elem.scrollIntoView() Calcula el desplazamiento necesario
elem.scrollIntoViewIfNeeded() No estándar, solo Chromium
elem.innerText Es consciente del layout: refleja lo renderizado
elem.focus() Puede provocar un scroll de desplazamiento y con él un layout
elem.select(), input.setSelectionRange() Selección de texto sobre geometría
elem.computedRole, elem.computedName El nombre accesible depende de lo renderizado
elem.checkVisibility() Comprueba visibilidad efectiva
range.getBoundingClientRect(), range.getClientRects() Geometría de un rango de texto
document.elementFromPoint(), document.elementsFromPoint() Hit testing sobre el árbol de cajas
document.caretPositionFromPoint() Igual, a nivel de carácter

innerText es el que más gente pilla desprevenida. Devuelve el texto tal como se ve: colapsa espacios según las reglas de renderizado, respeta text-transform, omite lo que está en display: none e inserta saltos de línea donde el layout los puso. Para saber todo eso hay que haber hecho el layout. Su hermano textContent no sabe nada de renderizado —devuelve el contenido textual del árbol tal cual— y por eso es gratis. Si solo quieres el texto, usa textContent. La regla práctica: si el nombre de la propiedad implica “lo que se ve”, fuerza layout; si implica “lo que hay”, no.

scrollTop merece un aviso aparte porque es de las poquísimas que fuerzan también al escribir. Asignar elem.scrollTop = 500 obliga al navegador a resolver el layout para saber si ese 500 es alcanzable y dónde queda; no es un simple guardar-y-seguir.

Ventana, getComputedStyle y SVG

En window fuerzan layout las lecturas de posición y tamaño del viewport:

API Fuerza
window.scrollX, window.scrollY Layout
window.pageXOffset, window.pageYOffset Layout (alias de las anteriores)
window.innerWidth, window.innerHeight Layout
window.visualViewport.width, .height, .offsetTop, .offsetLeft, .scale Layout
window.scrollTo(), window.scrollBy() Layout
window.getComputedStyle() Estilo siempre; layout según el caso

getComputedStyle es el caso especial de toda la lista y merece su propio párrafo. Llamarlo y leer cualquier propiedad siempre fuerza el recálculo de estilo. Además fuerza el layout completo si se cumple alguna de estas condiciones: el elemento está dentro de un shadow tree; la página usa media queries que dependen del viewport (width, height, aspect-ratio, orientation, resolution y familia); o la propiedad que pides es una de las que se resuelven contra la geometría, es decir width, height, inline-size, block-size, top, right, bottom, left, márgenes, paddings, transform, translate, rotate, scale, transform-origin, perspective-origin, filter, backdrop-filter, offset-path, offset-distance y la familia de grid-template. Leer color o font-family fuerza estilo pero no layout. Leer width fuerza los dos. Esta asimetría es lo que hace que getComputedStyle aparezca unas veces como “barato” y otras como “carísimo” en el mismo perfil.

En SVG la lista es larga porque casi toda la API de medida de texto y de cajas necesita el layout resuelto:

API de SVG Nota
svgElem.getBBox() Caja delimitadora en el sistema de coordenadas local
svgElem.getCTM(), getScreenCTM() Matrices de transformación acumuladas
textElem.getComputedTextLength() Longitud del texto renderizado
textElem.getNumberOfChars() Cuenta los caracteres renderizados
textElem.getStartPositionOfChar(), getEndPositionOfChar() Posición por carácter
textElem.getExtentOfChar(), getRotationOfChar() Geometría por carácter
textElem.getCharNumAtPosition() Hit testing de carácter
textElem.getSubStringLength(), selectSubString() Medida de subcadena
useElem.instanceRoot Resuelve el árbol clonado

Si dibujas un gráfico y calculas el ancho de cada etiqueta con getComputedTextLength() dentro del mismo bucle que las posiciona, tienes el mismo problema del nivel anterior con otro traje. La lista completa y actualizada de todo esto la mantiene Paul Irish en su gist What forces layout / reflow; merece un marcador en el navegador.

Por contraste, esto no fuerza nada y suele haber sustituto:

  • elem.textContent y elem.innerHTML al leer.
  • classList.add/remove/toggle, setAttribute, asignar a style.*: invalidan, pero no fuerzan.
  • IntersectionObserver y ResizeObserver: entregan geometría fuera de tu tarea, calculada por el motor cuando ya tenía el layout hecho. Son el sustituto correcto de casi cualquier medición periódica.
  • element.animate() y la API de animaciones web.
  • document.createElement y construir un árbol desconectado: sin conectar al documento no hay caja que invalidar.
La lista no se memoriza: se deduce de una sola pregunta

Nadie retiene cuarenta nombres de API, y no hace falta. Todas las entradas de esta lista responden que sí a la misma pregunta: ¿el valor que devuelve esta llamada podría cambiar si cambio el CSS sin tocar el HTML? Si la respuesta es sí, el valor es un producto del layout y la llamada lo va a forzar. textContent no cambia si cambias el CSS: es gratis. innerText sí cambia —basta un display: none o un text-transform: uppercase— y por eso es caro. getAttribute('class') no cambia: gratis. computedName sí, porque el nombre accesible ignora lo que no se renderiza: caro. getBBox() de un SVG cambia con cualquier font-size: caro. La heurística acierta en toda la lista y en las APIs que todavía no existen, que es lo valioso: dentro de tres años habrá métodos nuevos y la pregunta seguirá funcionando. Hay un corolario que es el que de verdad separa a quien ha sufrido esto. Fíjate en que el criterio no es “esta API es lenta”, sino “esta API tiene una dependencia de datos con una etapa posterior del pipeline”. Es exactamente el mismo razonamiento que aplicas cuando decides si una consulta a base de datos puede ir en un bucle: no importa lo rápida que sea la consulta, importa que introduce una dependencia que impide agrupar el trabajo. El layout forzado es la versión frontend del problema N+1, con la diferencia cruel de que aquí la “consulta” parece un acceso a un campo de un objeto y no un viaje a otro sistema. Cuando aprendes a leer rect.top como si fuera un await, ya no vuelves a colocarlo dentro de un bucle sin pensarlo.

Cómo usar la lista en la práctica

La lista no sirve para prohibir. Sirve para tres decisiones concretas.

Al escribir código, para saber si estás cruzando la frontera. Cuando una función lee algo de estas tablas, esa función pasa a ser una función de medición y deja de poder mezclarse con mutaciones. Marcarlo en el nombre ayuda más de lo que parece: medirFilas() frente a pintarFilas().

Al revisar código ajeno, para buscar el patrón, no la llamada. Una lectura sola no es sospechosa. Una lectura dentro de un bucle que también escribe lo es siempre. Una lectura dentro de un manejador de scroll o de resize lo es casi siempre.

Al leer un perfil, para saber qué buscar. Cuando el panel de rendimiento marca un aviso de forced reflow, te da la pila de llamadas: la línea culpable será una de esta lista. Si no la reconoces —y computedRole o getCharNumAtPosition no las reconoce casi nadie—, la tabla te ahorra media hora.

Un último matiz sobre la frontera estilo/layout. Forzar solo el recálculo de estilo es más barato que forzar el layout, pero no es gratis: sobre un documento con miles de elementos y selectores complejos, un recálculo completo se mide en decenas de milisegundos. La diferencia es que el estilo escala con el número de elementos invalidados por el número de reglas candidatas, mientras que el layout escala además con la profundidad y el tipo de contenedores. Por eso una página con muchísimo CSS y poco DOM sufre en estilo, y una con poco CSS y un DOM enorme sufre en layout.

⚔️ Construye tu propio detector
  1. Escribe una función que envuelva getBoundingClientRect en un Proxy o mediante redefinición y cuente cuántas veces se llama por fotograma. Instrumenta una página real durante un scroll.
  2. Comprueba empíricamente la asimetría de getComputedStyle: mide leer color frente a leer width sobre un documento con miles de nodos e invalidación pendiente.
  3. Sustituye innerText por textContent en un fragmento donde el resultado sea equivalente y mide la diferencia.
  4. Verifica que scrollTop fuerza layout también al escribir: escribe en él dentro de un bucle que además mute estilos y graba el perfil.
  5. Busca en tu código toda lectura de la primera tabla que esté dentro de un manejador de scroll. Anótalas: son la lista de deberes de la lección siguiente.