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.
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.
- 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.textContentyelem.innerHTMLal leer.classList.add/remove/toggle,setAttribute, asignar astyle.*: invalidan, pero no fuerzan.IntersectionObserveryResizeObserver: 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.createElementy construir un árbol desconectado: sin conectar al documento no hay caja que invalidar.
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.
- Escribe una función que envuelva
getBoundingClientRecten unProxyo mediante redefinición y cuente cuántas veces se llama por fotograma. Instrumenta una página real durante un scroll. - Comprueba empíricamente la asimetría de
getComputedStyle: mide leercolorfrente a leerwidthsobre un documento con miles de nodos e invalidación pendiente. - Sustituye
innerTextportextContenten un fragmento donde el resultado sea equivalente y mide la diferencia. - Verifica que
scrollTopfuerza layout también al escribir: escribe en él dentro de un bucle que además mute estilos y graba el perfil. - 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.