Sintaxis descriptivas

En este módulo, aprenderás cómo ofrecer al navegador una variedad de imágenes para que pueda tomar las mejores decisiones sobre qué mostrar. srcset no es un método para intercambiar fuentes de imágenes en puntos de interrupción específicos ni está diseñado para intercambiar una imagen por otra. Estas sintaxis permiten que el navegador resuelva un problema muy difícil, independiente de nosotros: solicitar y renderizar sin problemas una fuente de imagen adaptada al contexto de navegación del usuario, lo que incluye el tamaño del viewport, la densidad de la pantalla, las preferencias del usuario, el ancho de banda y muchos otros factores.

Es una gran exigencia, sin duda, más de lo que queremos considerar cuando solo marcamos una imagen para la Web, y hacerlo bien implica más información de la que podemos acceder.

Describe la densidad con x

Una <img> con un ancho fijo ocupará la misma cantidad del viewport en cualquier contexto de navegación, independientemente de la densidad de la pantalla del usuario, es decir, la cantidad de píxeles físicos que componen su pantalla. Por ejemplo, una imagen con un ancho inherente de 400px ocupará casi todo el viewport del navegador en el Google Pixel original y en el Pixel 6 Pro mucho más reciente (ambos dispositivos tienen una viewport normalizada de 412px píxeles lógicos).

Sin embargo, el Pixel 6 Pro tiene una pantalla mucho más nítida. Sin embargo, el Pixel 6 Pro tiene una resolución física de 1,440 × 3,120 píxeles, mientras que el Pixel es de 1,080 × 1,920 píxeles, es decir, la cantidad de píxeles de hardware que componen la pantalla.

La proporción entre los píxeles lógicos y los píxeles físicos de un dispositivo es la proporción de píxeles del dispositivo para esa pantalla (DPR). La DPR se calcula dividiendo la resolución real de la pantalla del dispositivo por los píxeles de CSS de un viewport.

Se muestra una DPR de 2 en la ventana de la consola.

Por lo tanto, el Pixel original tiene una DPR de 2.6, mientras que el Pixel 6 Pro tiene una DPR de 3.5.

El iPhone 4, el primer dispositivo con una DPR superior a 1, informa una proporción de píxeles del dispositivo de 2 (la resolución física de la pantalla es el doble de la resolución lógica). Cualquier dispositivo anterior al iPhone 4 tenía una DPR de 1: un píxel lógico por un píxel físico.

Si ves esa imagen de ancho 400px en una pantalla con una DPR de 2, cada píxel lógico se renderiza en cuatro de los píxeles físicos de la pantalla: dos horizontales y dos verticales. La imagen no se beneficia de la pantalla de alta densidad; se verá igual que en una pantalla con una DPR de 1. Por supuesto, todo lo que el motor de renderización del navegador "dibuje" (por ejemplo, texto, formas de CSS o SVG) se dibujará para adaptarse a la pantalla de mayor densidad. Sin embargo, como aprendiste en Formatos de imagen y compresión, las imágenes de trama son cuadrículas fijas de píxeles. Si bien no siempre resulta evidente, una imagen de trama ampliada para adaptarse a una pantalla de mayor densidad se verá de baja resolución en comparación con la página circundante.

Para evitar este aumento de tamaño, la imagen que se renderiza debe tener un ancho intrínseco de al menos 800 píxeles. Cuando se reduzca el tamaño para adaptarse a un espacio en un diseño de 400 píxeles lógicos de ancho, esa fuente de imagen de 800 píxeles tiene el doble de densidad de píxeles (en una pantalla con una DPR de 2), se verá agradable y nítida.

Primer plano de un pétalo de flor que muestra una disparidad en la densidad.

Como una pantalla con una DPR de 1 no puede usar la mayor densidad de una imagen, se reducirá la escala para que coincida con la pantalla y, como sabes, una imagen reducida se verá bien. En una pantalla de baja densidad, una imagen adecuada para pantallas de mayor densidad se verá como cualquier otra imagen de baja densidad.

Como aprendiste en Imágenes y rendimiento, un usuario con una pantalla de baja densidad que vea una fuente de imagen reducida a 400px solo necesitará una fuente con un ancho inherente de 400px. Si bien una imagen mucho más grande funcionaría para todos los usuarios visualmente, una fuente de imagen enorme de alta resolución renderizada en una pantalla pequeña y de baja densidad se verá como cualquier otra imagen pequeña de baja densidad, pero se sentirá mucho más lenta.

Como puedes suponer, los dispositivos móviles con una DPR de 1 son poco comunes, aunque de todas formas son comunes en los contextos de navegación de "computadoras de escritorio". Según datos compartidos por Matt Hobbs, aproximadamente el 18% de las sesiones de navegación de GOV.UK de noviembre de 2022 informan una DPR de 1. Si bien las imágenes de alta densidad se verán de la forma que podrían esperar esos usuarios, tendrán un ancho de banda y un costo de procesamiento mucho mayores, lo que resulta particularmente preocupante para los usuarios de dispositivos más antiguos y menos potentes que probablemente tengan pantallas de baja densidad.

El uso de srcset garantiza que solo los dispositivos con pantallas de alta resolución reciban fuentes de imagen lo suficientemente grandes como para verse bien, sin pasar el mismo costo de ancho de banda a los usuarios con pantallas de menor resolución.

El atributo srcset identifica uno o más candidatos separados por comas para renderizar una imagen. Cada candidato se compone de dos elementos: una URL, como la usarías en src, y una sintaxis que describe esa fuente de imagen. Cada candidato en srcset se describe por su ancho inherente ("sintaxis w") o su densidad prevista ("sintaxis x").

La sintaxis x es una abreviatura de "esta fuente es adecuada para una pantalla con esta densidad"; un candidato seguido de 2x es apropiado para una pantalla con una DPR de 2.

<img src="low-density.jpg" srcset="double-density.jpg 2x" alt="...">

Los navegadores que admitan srcset tendrán dos candidatos: double-density.jpg, que 2x describe como apropiado para pantallas con una DPR de 2, y low-density.jpg en el atributo src (el candidato seleccionado si no se encuentra nada más apropiado en srcset). En el caso de los navegadores que no admiten srcset, se ignorará el atributo y su contenido; los contenidos de src se solicitarán como de costumbre.

Es fácil confundir los valores especificados en el atributo srcset con instrucciones. Ese 2x informa al navegador que el archivo fuente asociado sería apto para usarse en una pantalla con una DPR de 2 (información sobre la fuente en sí). No le indica al navegador cómo usar esa fuente, solo le informa al navegador cómo podría usarse la fuente. Es una distinción sutil, pero importante: esta es una imagen de doble densidad, no una imagen para usar en una pantalla de doble densidad.

La diferencia entre una sintaxis que dice "esta fuente es adecuada para pantallas 2x" y una que dice "usar esta fuente en pantallas 2x" es leve en las imágenes impresas, pero la densidad de visualización es solo uno de la gran cantidad de factores intervinculados que el navegador utiliza para decidir la candidata a renderizar, solo algunos de los cuales puedes conocer. Por ejemplo, de forma individual, puedes determinar si un usuario habilitó una preferencia de navegador que ahorra ancho de banda a través de la consulta de medios prefers-reduced-data, y usarla para habilitar siempre a los usuarios imágenes de baja densidad, independientemente de la densidad de la pantalla, pero a menos que los desarrolladores la implementen de manera coherente en cada sitio web, no sería de mucha utilidad para un usuario. Es posible que se respete su preferencia en un sitio y que, en otro, se encuentre con una pared de imágenes que elimina el ancho de banda.

El algoritmo de selección de recursos que srcset/sizes usa deliberadamente poco claro permite que los navegadores decidan imágenes de menor densidad con disminuciones del ancho de banda o según una preferencia para minimizar el uso de datos, sin que nosotros asumamos la responsabilidad de cómo, cuándo o en qué umbral. No tiene sentido asumir responsabilidades (y trabajo adicional) que el navegador esté mejor preparado para ti.

Describe los anchos con w

srcset acepta un segundo tipo de descriptor para los candidatos de fuente de imagen. Es mucho más poderoso y, para nuestros fines, es mucho más fácil de entender. En lugar de marcar un candidato para que tenga las dimensiones adecuadas para una densidad de pantalla determinada, la sintaxis de w describe el ancho inherente de cada fuente candidata. De nuevo, cada candidato es idéntico, ya que tiene las mismas dimensiones: el mismo contenido, el mismo recorte y la misma relación de aspecto. Sin embargo, en este caso, quieres que el navegador del usuario elija entre dos candidatos: Small.jpg, una fuente con un ancho inherente de 600 px, y large.jpg, una fuente con un ancho inherente de 1,200 px.

srcset="small.jpg 600w, large.jpg 1200w"

Esto no le indica al navegador qué hacer con esta información, sino que solo le proporciona una lista de candidatos para mostrar la imagen. Antes de que el navegador pueda tomar una decisión sobre qué fuente renderizar, tienes que proporcionarle un poco más de información: una descripción de cómo se renderizará la imagen en la página. Para hacerlo, usa el atributo sizes.

Describe el uso con sizes

Los navegadores tienen un excelente rendimiento a la hora de transferir imágenes. Las solicitudes de recursos de imagen se iniciarán mucho antes de las solicitudes de hojas de estilo o JavaScript, a menudo incluso antes de que se haya analizado por completo el lenguaje de marcado. Cuando el navegador realiza estas solicitudes, no tiene información sobre la página en sí, además del lenguaje de marcado; es posible que aún no haya iniciado solicitudes para hojas de estilo externas, y mucho menos las haya aplicado. Cuando el navegador analiza el lenguaje de marcado y comienza a realizar solicitudes externas, solo cuenta con información a nivel del navegador: el tamaño del viewport del usuario, la densidad de píxeles de su pantalla, las preferencias del usuario, etcétera.

Esto no nos dice nada sobre cómo está diseñada una imagen para renderizarse en el diseño de página; ni siquiera puede usar el viewport como un proxy para el límite superior del tamaño de img, ya que puede ocupar un contenedor de desplazamiento horizontal. Por lo tanto, debemos proporcionarle al navegador esta información y hacerlo usando lenguaje de marcado. Eso es todo lo que podremos usar para estas solicitudes.

Al igual que srcset, sizes tiene como objetivo que la información sobre una imagen esté disponible tan pronto como se analice el lenguaje de marcado. Al igual que el atributo srcset es una abreviatura de "aquí están los archivos de origen y sus tamaños inherentes", el atributo sizes es la abreviación de "aquí es el tamaño de la imagen renderizada en el diseño". La forma en que describes la imagen es relativa al viewport; de nuevo, el tamaño del viewport es la única información de diseño que tiene el navegador cuando se realiza la solicitud de la imagen.

En los textos impresos, este puede parecer un poco complicado, pero es mucho más fácil de entender en la práctica:

<img
 sizes="80vw"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">

Aquí, este valor de sizes informa al navegador que el espacio en nuestro diseño que ocupa img tiene un ancho de 80vw: el 80% del viewport. Recuerda que esta no es una instrucción, sino una descripción del tamaño de la imagen en el diseño de la página. No dice "Haz que esta imagen ocupe el 80% del viewport", sino que "esta imagen terminará ocupando el 80% del viewport una vez que se renderice la página".

Como desarrollador, tu trabajo está hecho. Describiste con precisión una lista de fuentes candidatas en srcset y el ancho de tu imagen en sizes y, al igual que con la sintaxis x en srcset, el resto depende del navegador.

Sin embargo, para comprender completamente cómo se usa esta información, analicemos las decisiones que toma el navegador de un usuario cuando se encuentra con este lenguaje de marcado:

Indicaste al navegador que esta imagen ocupará el 80% del viewport disponible, por lo que, si volviéramos a renderizar este img en un dispositivo con un viewport de 1,000 píxeles de ancho, la imagen ocupará 800 píxeles. El navegador tomará ese valor y lo dividirá en función del ancho de cada una de las fuentes de imágenes candidatas que especificamos en srcset. La fuente más pequeña tiene un tamaño inherente de 600 píxeles, por lo tanto: 600 ÷ 800 = 0.75. Nuestra imagen mediana tiene 1200 píxeles de ancho: 1200÷800=1.5. Nuestra imagen más grande tiene 2,000 píxeles de ancho: 2000 ÷ 800=2.5.

Los resultados de esos cálculos (.75, 1.5 y 2.5) son, de hecho, opciones de DPR personalizadas específicamente para el tamaño del viewport del usuario. Debido a que el navegador también dispone de información sobre la densidad de pantalla del usuario, toma una serie de decisiones:

En este tamaño de viewport, el candidato small.jpg se descarta independientemente de la densidad de visualización del usuario. Con una DPR calculada inferior a 1, esta fuente requeriría un aumento de la escala para cualquier usuario, por lo que no es apropiado. En un dispositivo con una DPR de 1, medium.jpg proporciona la coincidencia más cercana, esa fuente es apropiada para mostrar con una DPR de 1.5, por lo que es un poco más grande de lo necesario, pero recuerda que el escalamiento vertical es un proceso visualmente fluido. En un dispositivo con una DPR de 2,large.jpg es la coincidencia más cercana, por lo que se selecciona.

Si la misma imagen se renderiza en un viewport de 600 píxeles de ancho, el resultado de todos los cálculos sería completamente diferente: 80vw ahora es de 480 píxeles. Cuando dividimos los anchos de nuestras fuentes con eso, obtenemos 1.25, 2.5 y 4.1666666667. Con este tamaño de viewport, se elegirá small.jpg en dispositivos 1x, y medium.jpg coincidirá en dispositivos 2x.

Esta imagen se verá idéntica en todos estos contextos de navegación: todos nuestros archivos fuente son exactamente iguales aparte de sus dimensiones y cada uno se renderiza con la misma nitidez que la densidad de visualización del usuario lo permite. Sin embargo, en lugar de entregar large.jpg a todos los usuarios para adaptarse a los viewports más grandes y las pantallas de mayor densidad, a los usuarios siempre se les mostrará el candidato más pequeño adecuado. Si usas una sintaxis descriptiva en lugar de una prescriptiva, no necesitas establecer interrupciones de forma manual ni considerar futuras viewports y DPR; solo debes proporcionar información al navegador y permitirle que determine las respuestas por ti.

Como nuestro valor sizes es relativo al viewport y es completamente independiente del diseño de la página, agrega una capa de complicación. No es común tener una imagen que solo ocupe un porcentaje del viewport, sin márgenes de ancho fijo, padding ni influencia de otros elementos de la página. Con frecuencia, necesitarás expresar el ancho de una imagen mediante una combinación de unidades: porcentajes, em, px, etcétera.

Afortunadamente, puedes usar calc() aquí. Cualquier navegador con compatibilidad nativa para imágenes responsivas también admitirá calc(), lo que nos permitirá mezclar y combinar unidades de CSS, por ejemplo, una imagen que ocupa todo el ancho del viewport del usuario, menos un margen 1em a ambos lados:

<img
    sizes="calc(100vw-2em)"
    srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w, x-large.jpg 2400w"
    src="fallback.jpg"
    alt="...">

Describe los puntos de interrupción

Si dedicaste mucho tiempo a trabajar con diseños responsivos, es probable que hayas notado que falta algo en estos ejemplos: el espacio que ocupa una imagen en un diseño probablemente cambie en los puntos de interrupción. En ese caso, debes pasar algunos detalles al navegador: sizes acepta un conjunto de candidatos separados por comas para el tamaño renderizado de la imagen, al igual que srcset acepta candidatos separados por comas para fuentes de imágenes. Esas condiciones usan la conocida sintaxis de consulta de medios. Esta sintaxis es la primera coincidencia: en cuanto coincide una condición multimedia, el navegador deja de analizar el atributo sizes y se aplica el valor especificado.

Supongamos que tienes una imagen destinada a ocupar el 80% del viewport, menos un em de padding en cada lado, en viewports de más de 1,200 px. En viewports más pequeños, ocupa todo el ancho del viewport.

  <img
     sizes="(min-width: 1200px) calc(80vw - 2em), 100vw"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

Si el viewport del usuario es superior a 1,200 px, calc(80vw - 2em) describe el ancho de la imagen en nuestro diseño. Si la condición (min-width: 1200px) no coincide, el navegador pasa al siguiente valor. Como no hay una condición de contenido multimedia específica vinculada a este valor, 100vw se usa como predeterminado. Si escribiras este atributo sizes con consultas de medios max-width, usa lo siguiente:

  <img
     sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

En términos sencillos: "¿Coincide (max-width: 1200px)? Si no es así, continúa. El siguiente valor, calc(80vw - 2em), no tiene una condición apta, por lo que este es el seleccionado.

Ahora que proporcionaste al navegador toda esta información sobre tu elemento img (fuentes potenciales, anchos inherentes y cómo pretendes presentar la imagen al usuario), el navegador usa un conjunto parcial de reglas para determinar qué hacer con esa información. Si eso suena poco claro, bueno, se debe a que lo es, por diseño. El algoritmo de selección de fuente codificado en la especificación HTML es explícitamente impreciso en cuanto a cómo debe elegirse una fuente. Una vez que se hayan analizado las fuentes, sus descriptores y la forma en que se renderizará la imagen, el navegador podrá hacer lo que quiera. No podrás saber con certeza qué fuente elegirá.

Una sintaxis que diga "usar esta fuente en una pantalla de alta resolución" sería predecible, pero no abordaría el problema principal de las imágenes en un diseño responsivo: la conservación del ancho de banda del usuario. La densidad de píxeles de una pantalla solo está relacionada de forma tangencial con la velocidad de la conexión a Internet (si corresponde). Si usas una laptop de primer nivel, pero navegas por la Web mediante una conexión de uso medido, mediante una conexión mediante tu teléfono o una conexión Wi-Fi de avión inestable, es posible que desees inhabilitar las fuentes de imágenes de alta resolución, independientemente de la calidad de tu pantalla.

Dejar la decisión final sobre el navegador permite muchas más mejoras en el rendimiento de las que podríamos administrar con una sintaxis estrictamente prescriptiva. Por ejemplo, en la mayoría de los navegadores, una img que use la sintaxis srcset o sizes nunca te molestará en solicitar una fuente con dimensiones más pequeñas que la que el usuario ya tiene en la caché de su navegador. ¿Cuál sería el punto de hacer una nueva solicitud para una fuente que se vería idéntica, cuando el navegador puede reducir la escala de la fuente de imagen que ya tiene sin problemas? Sin embargo, si el usuario escala su viewport hasta el punto en que se necesita una imagen nueva para evitar el aumento de la escala, esa solicitud se realizará de todos modos, por lo que todo se verá como esperas.

La falta de control explícito puede parecer un poco aterrador a nivel literal, pero, como usas archivos de origen con contenido idéntico, es más probable que les ofrezcamos a los usuarios una experiencia "dañada" que con un objeto src de una sola fuente, independientemente de las decisiones que tome el navegador.

Usa sizes y srcset

Es mucha información, tanto para ti como para el lector, y para el navegador. srcset y sizes son sintaxis densas que describen una cantidad impactante de información con muy pocos caracteres. Esto es, para mejor o peor, diseño: si estas sintaxis fueran menos concisas (y los humanos las analicen con mayor facilidad), podría haberlas dificultado el análisis para un navegador. Mientras más complejidad se agregue a una cadena, mayor será el potencial de errores del analizador o diferencias involuntarias en el comportamiento de un navegador a otro. Sin embargo, hay una ventaja aquí: una sintaxis que las máquinas pueden leer con mayor facilidad es una que escriben con mayor facilidad.

srcset es un caso claro para la automatización. No es común que diseñes manualmente varias versiones de tus imágenes para un entorno de producción. En lugar de eso, automatizarás el proceso con un ejecutor de tareas como Gulp, un agrupador como Webpack, una CDN de terceros como Cloudinary o una función ya integrada en el CMS que elijas. Si se proporciona información suficiente para generar nuestras fuentes en primer lugar, un sistema tendría la información necesaria para escribirlas en un atributo srcset viable.

sizes es un poco más difícil de automatizar. Como sabes, la única manera en que un sistema puede calcular el tamaño de una imagen en un diseño renderizado es haberlo renderizado. Afortunadamente, existen varias herramientas para desarrolladores que abstraen el proceso de escritura a mano de atributos sizes, con una eficiencia que nunca se podría igualar de forma manual. Por ejemplo, respImageLint es un fragmento de código destinado a examinar la precisión de tus atributos sizes y proporcionar sugerencias para mejorar. El proyecto Lazysizes compromete la velocidad para lograr la eficiencia, ya que difiere las solicitudes de imagen hasta que se haya establecido el diseño, lo que permite que JavaScript genere valores de sizes por ti. Si usas un framework de renderización completamente del cliente, como React o Vue, existen varias soluciones para crear o generar atributos srcset y sizes, que analizaremos con más detalle en CMS y frameworks.