API JavaScript Resize Observer
Aprende la API JavaScript Resize Observer para reaccionar a cambios de tamaño en elementos individuales: componentes responsivos, canvas y gráficos redimensionables e inputs que crecen automáticamente.
La API ResizeObserver te permite reaccionar cuando un elemento específico cambia de tamaño, sin importar la causa. Pertenece a la misma familia que el MutationObserver (que observa el árbol DOM) y el IntersectionObserver (que observa la visibilidad): observadores eficientes basados en callbacks que reemplazan los engorrosos bucles de sondeo.
¿Por qué no usar simplemente el evento resize?
El evento resize de window solo se dispara cuando cambia el tamaño de la ventana del navegador. Pero un elemento en la página puede cambiar de tamaño por muchas otras razones:
- Una regla CSS cambia (se activa una media query, se alterna una clase).
- Su contenido cambia (se carga texto, llega una imagen, se agregan filas a una lista).
- Un layout de Flexbox o CSS Grid se redistribuye porque un elemento hermano creció o se redujo.
- Un contenedor padre es redimensionado por un divisor arrastrable, una barra lateral o un panel.
Ninguno de estos necesariamente redimensiona la ventana, por lo que window.addEventListener('resize', ...) nunca se dispara. Podrías usar sondeo con setInterval y comparar getBoundingClientRect() cada pocos milisegundos, pero eso desperdicia CPU y sigue siendo lento respecto al cambio real.
ResizeObserver resuelve esto: observa uno o más elementos y te llama solo cuando su tamaño realmente cambia.
ResizeObserver informa el tamaño del contenido de un elemento por defecto, no su posición. Si necesitas saber cuándo un elemento se mueve o aparece en el viewport, usa IntersectionObserver en su lugar. Para mediciones a nivel de viewport, consulta tamaños de ventana y desplazamiento.
Uso básico
El patrón refleja el de los otros observadores: crea un observador con un callback y luego indícale qué elemento(s) debe observe.
const box = document.querySelector('#box');
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`${entry.target.id} is now ${width} x ${height}`);
}
});
ro.observe(box);El callback recibe un array de entradas — una por elemento observado que cambió en este lote — por lo que un solo observador puede vigilar muchos elementos a la vez.
Qué contiene una entrada
Cada entrada describe un elemento y su nuevo tamaño. Hay dos formas de leer ese tamaño.
La forma sencilla es entry.contentRect, un DOMRectReadOnly con width, height, top y left (los desplazamientos son relativos al cuadro de relleno del elemento):
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log(entry.target); // the element
console.log(entry.contentRect.width); // content-box width in px
console.log(entry.contentRect.height); // content-box height in px
}
});La forma más precisa usa las propiedades de tamaño de caja, que son arrays de objetos { inlineSize, blockSize } (un array porque un elemento puede estar fragmentado entre columnas):
entry.contentBoxSize— el cuadro de contenido, excluyendo relleno y borde.entry.borderBoxSize— el cuadro de borde, incluyendo relleno y borde.entry.devicePixelContentBoxSize— el cuadro de contenido medido en píxeles de dispositivo, ideal para renderizado nítido en canvas en pantallas de alta densidad.
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
// inlineSize ~ width, blockSize ~ height (for a horizontal writing mode)
const { inlineSize, blockSize } = entry.borderBoxSize[0];
console.log(`border box: ${inlineSize} x ${blockSize}`);
}
});Por defecto el observador reacciona a cambios en el cuadro de contenido. Para observar el cuadro de borde en su lugar, pasa un objeto de opciones:
ro.observe(box, { box: 'border-box' });inlineSize y blockSize son conscientes del modo de escritura. En el modo de izquierda a derecha y de arriba a abajo por defecto, inlineSize es el ancho y blockSize es la altura — pero en un modo de escritura vertical se intercambian. Prefiere estas propiedades sobre width/height codificados cuando tu interfaz debe soportar múltiples direcciones de escritura.
Métodos
ResizeObserver expone tres métodos:
observe(element, options)— comienza a observar un elemento. Llámalo una vez por elemento. El parámetro opcionaloptions.boxpuede ser'content-box'(predeterminado),'border-box'o'device-pixel-content-box'.unobserve(element)— deja de observar un único elemento.disconnect()— deja de observar todos los elementos a la vez.
ro.observe(el); // start
ro.unobserve(el); // stop watching this one
ro.disconnect(); // stop watching everythingCaso de uso 1 — Componentes responsivos ("Container Queries en JS")
Una media query reacciona al viewport. Pero una tarjeta reutilizable puede ser ancha en la columna principal y estrecha en una barra lateral con el mismo ancho de viewport. Con ResizeObserver puedes adaptar un componente a su propio ancho:
const card = document.querySelector('.card');
const ro = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width;
// Toggle a layout class based on the element's own width.
card.classList.toggle('card--compact', width < 400);
});
ro.observe(card);.card { display: flex; gap: 1rem; }
.card--compact { flex-direction: column; }Ahora la tarjeta se apila verticalmente cuando ella misma tiene menos de 400px de ancho, independientemente del tamaño de la ventana — útil en paneles de control, paneles divididos y widgets incrustables.
El CSS moderno puede hacer esto de forma nativa con las container queries (@container). Si solo necesitas cambiar estilos basándote en el tamaño de un contenedor, prefiere las container queries de CSS — son declarativas y se ejecutan fuera del hilo principal. Usa ResizeObserver cuando necesites ejecutar JavaScript en respuesta al cambio de tamaño (recalcular valores, redibujar un canvas, redistribuir un gráfico).
Caso de uso 2 — Redimensionar un canvas o gráfico
Un <canvas> tiene dos tamaños: su tamaño de visualización CSS y el tamaño de su búfer de dibujo (canvas.width/canvas.height). Si no coinciden, el mapa de bits se estira y se ve borroso. ResizeObserver mantiene el búfer sincronizado con el tamaño mostrado para que los dibujos permanezcan nítidos:
const canvas = document.querySelector('#chart');
const ctx = canvas.getContext('2d');
const ro = new ResizeObserver(([entry]) => {
// Use device-pixel size for sharp rendering on high-DPI screens.
const size = entry.devicePixelContentBoxSize?.[0];
const width = size ? size.inlineSize : entry.contentRect.width;
const height = size ? size.blockSize : entry.contentRect.height;
canvas.width = width;
canvas.height = height;
redraw(ctx, width, height); // your drawing / charting code
});
ro.observe(canvas, { box: 'device-pixel-content-box' });La misma idea se aplica a las bibliotecas de gráficos: observa el contenedor del gráfico y llama al método resize() de la biblioteca cuando el contenedor cambie, en lugar de engancharte solo a window.resize.
Caso de uso 3 — Textarea que crece automáticamente
Dado que el callback se dispara cada vez que el tamaño medido del elemento cambia, puedes mantener elementos dependientes sincronizados. Un ejemplo clásico es reflejar la altura de un textarea en un elemento hermano, o reaccionar al crecimiento impulsado por el contenido:
const textarea = document.querySelector('#message');
const counter = document.querySelector('#height-readout');
const ro = new ResizeObserver(([entry]) => {
const h = Math.round(entry.contentRect.height);
counter.textContent = `${h}px tall`;
});
ro.observe(textarea);Cada vez que el usuario arrastra el controlador de redimensionado del textarea — o tu código cambia su altura mientras el usuario escribe — el indicador se actualiza automáticamente, sin evento resize y sin sondeo.
La advertencia del "bucle de ResizeObserver"
Es posible que encuentres este mensaje en la consola:
ResizeObserver loop completed with undelivered notifications.
(Algunos navegadores lo formulan como "ResizeObserver loop limit exceeded.") Ocurre cuando tu callback cambia el tamaño del elemento observado, lo que vuelve a activar el observador, que cambia el tamaño de nuevo — un bucle de retroalimentación.
// Anti-pattern: this can cause the loop warning.
const ro = new ResizeObserver(([entry]) => {
// Resizing the observed element from inside its own callback. Bad.
entry.target.style.height = entry.contentRect.width + 'px';
});No redimensiones sincrónicamente el elemento observado dentro de su propio callback. Si un redimensionado controlado por tamaño es inevitable, rompe el ciclo: escribe solo cuando el valor realmente cambia (una guarda), o difiere la escritura con requestAnimationFrame. La advertencia suele ser inofensiva y se autocorrige, pero un verdadero bucle infinito afectará el rendimiento.
Limpieza
Un observador mantiene una referencia a cada elemento que vigila, lo que puede impedir que ese elemento sea recolectado por el recolector de basura. Siempre deja de observar cuando hayas terminado — por ejemplo, cuando un componente se desmonta o una vista se destruye:
function mountWidget(el) {
const ro = new ResizeObserver(handleResize);
ro.observe(el);
// Return a cleanup function.
return () => ro.disconnect();
}Olvidar esto es una fuente habitual de fugas de memoria en aplicaciones de página única. Para más información sobre cómo mantener el trabajo de tamaño y maquetación eficiente, consulta optimización del rendimiento del DOM.
ResizeObserver está soportado en todos los navegadores modernos, por lo que puedes usarlo sin polyfill en cualquier navegador evergreen actual.