Reservar espacio: los atributos, aspect-ratio y por qué funcionan juntos
Por qué el navegador deriva una relación de aspecto de los atributos width y height, qué condición tiene que cumplir tu CSS para que sirva de algo, y cómo reservar espacio cuando no conoces las dimensiones.
Durante veinte años los atributos width y height de una imagen fueron el tamaño de presentación, y las hojas de estilo modernas los anulaban para hacer imágenes fluidas. En 2019 los motores cambiaron su significado: pasaron a ser también la fuente de una relación de aspecto implícita, que es exactamente la información que falta para dibujar la caja antes de que el fichero llegue. Ese cambio resolvió de golpe la mitad del CLS de la web, y sigue sin aplicarse en muchos sitios porque la regla tiene una condición que no es evidente.
- Explicar qué regla de la hoja de estilos del navegador convierte los atributos en relación de aspecto.
- Identificar la condición del CSS del autor sin la cual esa relación no sirve.
- Reservar espacio en contenedores cuando las dimensiones son desconocidas.
- Auditar un documento entero en busca de cajas sin espacio reservado.
La regla que casi nadie ha leído
Cuando el navegador encuentra una imagen con los dos atributos, aplica una regla equivalente a esta, que vive en su propia hoja de estilos:
img[width][height] {
aspect-ratio: attr(width) / attr(height);
}
Los atributos siguen mapeándose además a una pista de presentación de width y height en píxeles, como siempre. Lo nuevo es la relación de aspecto, y su valor es que está disponible en el primer layout, antes de que el navegador sepa nada del fichero. Con la relación y una sola dimensión, la otra sale por aritmética.
El soporte llegó en cuestión de meses en los tres motores: primero Firefox a finales de 2019, después Chromium, y Safari en 2020. Desde 2021 no hay ningún navegador relevante que no lo haga, así que la técnica es simplemente la correcta.
Y aquí está la condición que hace que funcione o no funcione, y que es la razón de que tantos sitios tengan los atributos puestos y sigan desplazando:
La relación de aspecto solo produce una caja correcta si una de las dos dimensiones resuelve a auto.
Si tu CSS fija las dos, la relación no tiene nada que calcular y se ignora. Si tu CSS fija solo la anchura, la altura sigue viniendo de la pista de presentación —el valor del atributo en píxeles— y la imagen sale deformada, no reservada. Por eso la línea que hay que tener en el CSS base no es opcional:
img,
video {
max-inline-size: 100%;
block-size: auto; /* imprescindible: deja que la relacion calcule la altura */
}
Con esa línea puesta, el marcado correcto es el mínimo de siempre, y los atributos son la proporción, no el tamaño:
<img src="cala.avif" width="1600" height="900" alt="Cala de arena negra al atardecer">
Los números pueden ser 1600 y 900 o 16 y 9: lo único que se usa es su cociente. Poner las dimensiones intrínsecas reales es mejor práctica porque también informa al selector de recursos, pero para la reserva de espacio da igual.
El resto de elementos con caja
<video> funciona exactamente igual, con la misma regla y la misma condición. Su tamaño por defecto sin atributos es de 300 por 150 píxeles, que casi nunca coincide con el vídeo real.
<iframe> también tiene 300 por 150 por defecto y también admite los atributos, pero rara vez conoces la proporción del contenido incrustado. Ahí lo correcto es fijar la relación en el CSS del contenedor.
<source> dentro de <picture> admite width y height desde hace varios años, y su valor se hereda al <img> cuando esa fuente es la elegida. Es la única forma de reservar espacio correctamente cuando las variantes tienen proporciones distintas por diseño, que es el patrón de dirección de arte:
<picture>
<source media="(min-width: 800px)" srcset="hero-ancho.avif" width="2400" height="1000">
<source srcset="hero-cuadrado.avif" width="1080" height="1080">
<img src="hero-cuadrado.avif" width="1080" height="1080" alt="Fachada del mercado central">
</picture>
Sin esos atributos en cada <source>, la reserva se hace con la proporción del <img> de respaldo, y al elegirse la variante ancha la caja cambia de forma en cuanto llega el fichero.
Y la trampa correspondiente en srcset con descriptores de anchura: todas las candidatas de un mismo srcset tienen que compartir la proporción de los atributos. No hay forma de declarar una proporción por candidata, y si una variante está recortada distinto, la caja reservada es la equivocada.
Cuando no conoces las dimensiones
Contenido subido por usuarios, incrustaciones de terceros, componentes genéricos que aceptan cualquier imagen. La solución es fijar la relación en el contenedor y recortar dentro:
.marco {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.marco > img,
.marco > iframe,
.marco > video {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
La caja mide lo que tiene que medir desde el primer layout, y object-fit: cover se encarga de que la imagen llene el hueco sin deformarse, a costa de recortar. Cuando recortar no es aceptable —una obra de arte, un plano técnico— la alternativa es contain y aceptar las bandas.
Para contenido de altura genuinamente variable que no admite una proporción fija, la herramienta es min-block-size con un valor derivado de datos, no inventado:
.ranura-anuncio {
min-block-size: 280px; /* percentil 90 de las alturas servidas, medido */
contain: layout;
}
Y la cifra de ese min-block-size sale de medir, no de estimar. Un ResizeObserver durante una semana en producción da la distribución real de alturas de esa ranura; el percentil 90 es un buen punto de partida porque reserva de más para nueve de cada diez casos y evita el desplazamiento en todos ellos.
La reacción natural al descubrir el CLS es reservar generosamente: si la ranura puede medir entre 100 y 400 píxeles, reservo 400 y no se mueve nada nunca. Es correcto para la métrica y a menudo equivocado para el usuario, y conviene entender exactamente por qué antes de aplicarlo en todas partes.
El hueco reservado y no usado empuja el contenido real hacia abajo. Cuatrocientos píxeles de vacío entre la cabecera y el primer párrafo, en una ventana de móvil de 700 píxeles de alto, significan que más de la mitad de la primera pantalla está en blanco. Eso no aparece en ninguna métrica y es peor experiencia que un desplazamiento pequeño.
Y hay un efecto medible: puede desplazar el elemento del LCP fuera de la ventana. Si el candidato a mayor elemento con contenido queda por debajo del pliegue, deja de ser candidato, y el navegador elige otro más pequeño y más tardío. He visto reservas generosas empeorar el LCP en más de medio segundo por esta vía, sin tocar nada relacionado con imágenes.
La cuenta correcta es comparar el coste de las dos opciones en la misma unidad, y esa unidad es la puntuación de desplazamiento.
Un desplazamiento vertical de d píxeles que afecta a una fracción f del área visible, en una ventana cuya dimensión mayor es M, puntúa aproximadamente f * d / M. Con una ventana de móvil típica de 390 por 844, M vale 844.
| Escenario | d | f | Puntuación |
|---|---|---|---|
| Banner de 60 px arriba del todo | 60 | ~1,0 | 0,071 |
| Imagen de 200 px a media página | 200 | ~0,6 | 0,142 |
| Ranura de anuncio de 250 px arriba | 250 | ~1,0 | 0,296 |
| Ajuste de 4 px por cambio de fuente | 4 | ~0,9 | 0,004 |
La tabla dice tres cosas útiles de golpe. Que la posición importa tanto como el tamaño: los mismos 200 píxeles arriba del todo puntúan casi el doble que a media página, porque afectan a más superficie visible. Que un solo bloque grande sin reservar agota el presupuesto entero de 0,1 él solo. Y que los ajustes de pocos píxeles son casi gratis, lo cual es la licencia para no obsesionarse con la última décima de milímetro en el ajuste de fuentes.
De ahí sale el criterio de reserva que uso, que es intermedio y no maximalista:
Por encima del pliegue, reserva el máximo posible. Ahí f vale casi 1 y cualquier movimiento se paga completo. Y si eso deja un hueco feo, la respuesta correcta no es reservar menos: es no poner ahí contenido de altura impredecible.
Por debajo del pliegue, reserva la mediana y acepta el ajuste. Un desplazamiento de 80 píxeles a dos pantallas de distancia, con una f pequeña porque solo afecta a lo que quede visible, puntúa alrededor de 0,02. Es un precio razonable por no dejar huecos.
Fuera de la ventana, no reserves nada. Los elementos que no están en la ventana no contribuyen a la fracción de impacto. Cargar contenido diferido por debajo de lo visible es gratis desde el punto de vista del CLS, siempre que el usuario no esté a punto de llegar ahí. Es la razón técnica de que el margen de raíz del observador de intersección importe: cargar con 200 píxeles de antelación da tiempo a que la caja se estabilice antes de entrar en escena.
Auditar el documento entero
La comprobación que encuentra el problema en treinta segundos, y que conviene ejecutar en cada página importante:
const sospechosos = [...document.querySelectorAll('img, iframe, video, embed, object')]
.filter((el) => {
const tieneAtributos = el.hasAttribute('width') && el.hasAttribute('height');
const ar = getComputedStyle(el).aspectRatio;
const tieneRatio = ar && ar !== 'auto';
const alturaFija = getComputedStyle(el).blockSize !== 'auto';
return !(tieneAtributos || tieneRatio || alturaFija);
});
console.table(sospechosos.map((el) => ({
etiqueta: el.tagName.toLowerCase(),
clase: el.className.slice(0, 30),
recurso: (el.currentSrc || el.src || '').split('/').pop()?.slice(0, 40),
visible: el.getBoundingClientRect().top < innerHeight,
})));
La columna visible es la que ordena el trabajo: los sospechosos que están dentro de la primera pantalla son los caros.
Y la verificación definitiva, que no depende de ninguna heurística: bloquea las imágenes en el panel de red y recarga. Los patrones de bloqueo aceptan comodines, así que con *.avif, *.webp y *.jpg cubres casi todo. Si la maquetación de la página bloqueada es idéntica a la de la página cargada, el espacio está reservado. Si se desmonta, tienes delante el CLS que van a ver tus usuarios con conexión lenta, sin haber tenido que estrangular nada.
Ejecuta la auditoría en tus tres plantillas más visitadas y arregla solo los sospechosos con visible en verdadero. Después haz la prueba del bloqueo de imágenes antes y después, y captura las dos maquetaciones. La comparación visual es el argumento que convence a un equipo mucho más rápido que una décima de métrica.