Imágenes con valores altos de DPI para densidades de píxeles variables

Una de las características del complejo panorama de dispositivos actual es que hay una gama muy amplia de densidades de píxeles de pantalla disponibles. Algunos dispositivos tienen pantallas de muy alta resolución, mientras que otros se quedan atrás. Los desarrolladores de aplicaciones deben admitir una variedad de densidades de píxeles, lo que puede ser bastante desafiante. En la Web móvil, los desafíos se multiplican por varios factores:

  • Gran variedad de dispositivos con diferentes factores de forma.
  • Ancho de banda de red y duración de batería limitados.

En términos de imágenes, el objetivo de los desarrolladores de apps web es publicar las imágenes de mejor calidad de la manera más eficiente posible. En este artículo, se analizarán algunas técnicas útiles para hacerlo hoy y en un futuro cercano.

Evita las imágenes si es posible

Antes de abrir este melón, recuerda que la Web tiene muchas tecnologías potentes que son independientes de la resolución y el DPI. Específicamente, el texto, SVG y gran parte del CSS “funcionarán” debido a la función de escalamiento automático de píxeles de la Web (a través de devicePixelRatio).

Dicho esto, no siempre puedes evitar las imágenes rasterizadas. Por ejemplo, es posible que se te proporcionen elementos que serían bastante difíciles de replicar en SVG/CSS puro, o bien que estés tratando con una fotografía. Si bien puedes convertir la imagen en SVG automáticamente, vectorizar fotografías no tiene mucho sentido, ya que las versiones ampliadas suelen no verse bien.

Segundo plano

Una breve historia de la densidad de la pantalla

En sus inicios, las pantallas de las computadoras tenían una densidad de píxeles de 72 o 96 dpi (puntos por pulgada).

Las pantallas mejoran gradualmente la densidad de píxeles, gracias principalmente al caso de uso para dispositivos móviles, en el que los usuarios suelen acercar sus teléfonos al rostro, lo que hace que los píxeles sean más visibles. En 2008, los teléfonos de 150 dpi se convirtieron en la nueva norma. La tendencia de aumentar la densidad de la pantalla continuó, y los teléfonos nuevos de hoy en día tienen pantallas de 300 ppp (Apple las denomina "Retina").

Por supuesto, el Santo Grial es una pantalla en la que los píxeles son completamente invisibles. Para el factor de forma del teléfono, la generación actual de pantallas Retina/HiDPI puede ser similar a ese ideal. Sin embargo, es probable que las nuevas clases de hardware y wearables, como Project Glass, sigan aumentando la densidad de píxeles.

En la práctica, las imágenes de baja densidad deben tener el mismo aspecto en las pantallas nuevas que en las anteriores, pero, en comparación con las imágenes nítidas de alta densidad que los usuarios suelen ver, las imágenes de baja densidad se ven discordantes y pixeladas. La siguiente es una simulación aproximada de cómo se verá una imagen de 1x en una pantalla de 2x. En cambio, la imagen del doble de tamaño se ve bastante bien.

Baboon 1x
Baboon 2x
Baboons! en diferentes densidades de píxeles.

Píxeles en la Web

Cuando se diseñó la Web, el 99% de las pantallas tenían 96 ppp (o pretendían serlo) y se tomaron pocas medidas para las variaciones en este aspecto. Debido a la gran variación en los tamaños y las densidades de pantalla, necesitábamos una forma estándar de hacer que las imágenes se vean bien en una variedad de densidades y dimensiones de pantalla.

Recientemente, la especificación HTML abordó este problema definiendo un píxel de referencia que los fabricantes usan para determinar el tamaño de un píxel CSS.

Con el píxel de referencia, un fabricante puede determinar el tamaño del píxel físico del dispositivo en relación con el píxel estándar o ideal. Esta proporción se denomina proporción de píxeles del dispositivo.

Cómo calcular la proporción de píxeles del dispositivo

Supongamos que un teléfono inteligente tiene una pantalla con un tamaño de píxeles físico de 180 píxeles por pulgada (ppi). Para calcular la relación de píxeles del dispositivo, debes seguir tres pasos:

  1. Compara la distancia real a la que se sostiene el dispositivo con la distancia del píxel de referencia.

    Según las especificaciones, sabemos que, a 28 pulgadas, lo ideal es 96 píxeles por pulgada. Sin embargo, como se trata de un smartphone, las personas sostienen el dispositivo más cerca de la cara que una laptop. Supongamos que esa distancia es de 45 cm.

  2. Multiplica la relación de distancia por la densidad estándar (96 ppi) para obtener la densidad de píxeles ideal para la distancia determinada.

    idealPixelDensity = (28/18) × 96 = 150 píxeles por pulgada (aproximadamente)

  3. Toma la proporción de la densidad de píxeles física a la densidad de píxeles ideal para obtener la proporción de píxeles del dispositivo.

    devicePixelRatio = 180/150 = 1.2

Cómo se calcula devicePixelRatio
Diagrama que muestra un píxel angular de referencia para ayudar a ilustrar cómo se calcula devicePixelRatio.

Ahora, cuando un navegador necesita saber cómo cambiar el tamaño de una imagen para que se ajuste a la pantalla según la resolución ideal o estándar, el navegador se refiere a la relación de píxeles del dispositivo de 1.2, que indica que, por cada píxel ideal, este dispositivo tiene 1.2 píxeles físicos. La fórmula para pasar de los píxeles ideales (como se define en las especificaciones web) a los píxeles físicos (puntos en la pantalla del dispositivo) es la siguiente:

physicalPixels = window.devicePixelRatio * idealPixels

Históricamente, los proveedores de dispositivos tienden a redondear los devicePixelRatios (DPR). Los iPhone y iPad de Apple informan una DPR de 1, y sus equivalentes con Retina, de 2. La especificación de CSS recomienda lo siguiente:

La unidad de píxeles hace referencia al número entero de píxeles del dispositivo que se aproxima mejor al píxel de referencia.

Una razón por la que las relaciones redondas pueden ser mejores es porque pueden generar menos artefactos de subpíxeles.

Sin embargo, la realidad del panorama de dispositivos es mucho más variada, y los teléfonos Android suelen tener DPR de 1.5. La tablet Nexus 7 tiene un DPR de aproximadamente 1.33, que se obtuvo mediante un cálculo similar al anterior. Se espera que en el futuro haya más dispositivos con DPR variables. Por este motivo, nunca debes suponer que tus clientes tendrán DPR de números enteros.

Descripción general de las técnicas de imágenes HiDPI

Existen muchas técnicas para resolver el problema de mostrar las imágenes de mejor calidad lo más rápido posible, que se dividen en dos categorías:

  1. Optimizar imágenes únicas
  2. Optimización de la selección entre varias imágenes

Enfoques de una sola imagen: usa una imagen, pero haz algo inteligente con ella. Estos enfoques tienen el inconveniente de que, inevitablemente, sacrificarás el rendimiento, ya que descargarás imágenes HiDPI incluso en dispositivos más antiguos con DPI más bajos. Estos son algunos enfoques para el caso de una sola imagen:

  • Imagen HiDPI muy comprimida
  • Formato de imagen totalmente genial
  • Formato de imagen progresiva

Enfoques de varias imágenes: Usa varias imágenes, pero haz algo inteligente para elegir cuál cargar. Estos enfoques tienen una sobrecarga inherente para que el desarrollador cree varias versiones del mismo recurso y, luego, averigüe una estrategia de decisión. Dispone de las siguientes opciones:

  • JavaScript
  • Publicación del servidor
  • Consultas de medios de CSS
  • Funciones integradas del navegador (image-set(), <img srcset>)

Imagen de HiDPI muy comprimida

Las imágenes ya representan un asombroso 60% del ancho de banda que se usa para descargar un sitio web promedio. Si entregamos imágenes HiDPI a todos los clientes, podremos aumentar esta cantidad. ¿Cuánto crecerá?

Ejecuté algunas pruebas que generaron fragmentos de imagen de 1x y 2x con calidad JPEG en 90, 50 y 20. Esta es la secuencia de comandos de shell que usé (con ImageMagick) para generarlas:

Ejemplo 1 de tarjetas. Ejemplo 2 de tarjetas Ejemplo 3 de tarjetas
Muestras de imágenes con diferentes compresiones y densidades de píxeles.

A partir de este pequeño muestreo no científico, parece que comprimir imágenes grandes proporciona una buena relación calidad-tamaño. En mi opinión, las imágenes 2x muy comprimidas se ven mejor que las imágenes 1x sin comprimir.

Por supuesto, entregar imágenes de baja calidad y muy comprimidas a dispositivos de 2x es peor que entregar imágenes de mayor calidad, y el enfoque anterior genera penalizaciones de calidad de imagen. Si comparas la calidad de 90 imágenes con la de 20, verás una disminución de la nitidez y un aumento de la granulosidad. Es posible que estos artefactos no sean aceptables en los casos en los que las imágenes de alta calidad son clave (por ejemplo, una aplicación de visualización de fotos) o para los desarrolladores de apps que no están dispuestos a ceder.

La comparación anterior se realizó en su totalidad con JPEG comprimidos. Vale la pena señalar que hay muchos inconvenientes entre los formatos de imagen ampliamente implementados (JPEG, PNG y GIF), lo que nos lleva a…

Formato de imagen totalmente increíble

WebP es un formato de imagen atractivo bastante atractivo que se comprime muy bien y mantiene la alta fidelidad de la imagen. Por supuesto, aún no se implementó en todas partes.

Una forma de verificar la compatibilidad con WebP es a través de JavaScript. Cargas una imagen de 1 píxel a través de data-uri, esperas a que se cargue o se activen eventos de error y, luego, verificas que el tamaño sea correcto. Modernizr se envía con una secuencia de comandos de detección de funciones, que está disponible a través de Modernizr.webp.

Sin embargo, una mejor manera de hacerlo es directamente en CSS con la función image(). Por lo tanto, si tienes una imagen WebP y un resguardo JPEG, puedes escribir lo siguiente:

#pic {
  background: image("foo.webp", "foo.jpg");
}

Este enfoque tiene algunos problemas. En primer lugar, image() no está implementado de forma generalizada. En segundo lugar, si bien la compresión de WebP supera a JPEG, sigue siendo una mejora relativamente incremental, aproximadamente un 30% más pequeña según esta galería de WebP. Por lo tanto, solo WebP no es suficiente para abordar el problema de los valores altos de DPI.

Formatos de imagen progresiva

Los formatos de imagen progresivos, como JPEG 2000, JPEG progresivo, PNG progresivo y GIF, tienen el beneficio (algo debatido) de ver la imagen antes de que se cargue por completo. Pueden generar una sobrecarga de tamaño, aunque hay evidencia contradictoria sobre esto. Jeff Atwood afirmó que el modo progresivo “agrega alrededor de un 20% al tamaño de las imágenes PNG y un 10% al tamaño de las imágenes JPEG y GIF”. Sin embargo, Stoyan Stefanov afirmó que, para los archivos grandes, el modo progresivo es más eficiente (en la mayoría de los casos).

A primera vista, las imágenes progresivas se ven muy prometedoras en el contexto de publicar imágenes con la mejor calidad lo más rápido posible. La idea es que el navegador pueda dejar de descargar y decodificar una imagen una vez que sepa que los datos adicionales no aumentarán la calidad de la imagen (es decir, todas las mejoras de fidelidad son subpíxeles).

Si bien las conexiones son fáciles de finalizar, a menudo es costoso reiniciarlas. En el caso de un sitio con muchas imágenes, el enfoque más eficiente es mantener una sola conexión HTTP activa y reutilizarla el mayor tiempo posible. Si la conexión se cierra de forma prematura porque se descargó una imagen suficiente, el navegador debe crear una conexión nueva, lo que puede ser muy lento en entornos de baja latencia.

Una solución alternativa a esto es usar la solicitud de rango HTTP, que permite que los navegadores especifiquen un rango de bytes para recuperar. Un navegador inteligente podría realizar una solicitud HEAD para obtener el encabezado, procesarlo, decidir qué parte de la imagen se necesita y, luego, recuperarla. Lamentablemente, el rango HTTP no es compatible con los servidores web, lo que hace que este enfoque no sea práctico.

Por último, una limitación obvia de este enfoque es que no puedes elegir qué imagen cargar, solo puedes variar las fidelidades de la misma imagen. Como resultado, no se aborda el caso de uso de "dirección de arte".

Usa JavaScript para decidir qué imagen cargar

El primer enfoque y el más obvio para decidir qué imagen cargar es usar JavaScript en el cliente. Este enfoque te permite averiguar todo sobre tu usuario-agente y hacer lo correcto. Puedes determinar la relación de píxeles del dispositivo a través de window.devicePixelRatio, obtener el ancho y la altura de la pantalla e incluso posiblemente examinar algunas conexiones de red a través de navigator.connection o emitir una solicitud falsa, como lo hace la biblioteca foresight.js. Una vez que hayas recopilado toda esta información, puedes decidir qué imagen cargar.

Hay aproximadamente un millón de bibliotecas de JavaScript que hacen algo como lo anterior y, lamentablemente, ninguna de ellas es particularmente sobresaliente.

Una gran desventaja de este enfoque es que el uso de JavaScript implica que retrasarás la carga de imágenes hasta que el analizador de lectura previa haya terminado. En esencia, significa que ni siquiera las imágenes comenzarán a descargarse hasta después de que se active el evento pageload. Obtén más información en el artículo de Jason Grigsby.

Decide qué imagen cargar en el servidor

Puedes aplazar la decisión al servidor escribiendo controladores de solicitudes personalizados para cada imagen que publiques. Un controlador de este tipo verificaría la compatibilidad con Retina en función del usuario-agente (la única información que se transmite al servidor). Luego, según si la lógica del servidor quiere entregar recursos HiDPI, cargas el recurso adecuado (con un nombre según alguna convención conocida).

Lamentablemente, el usuario-agente no siempre proporciona información suficiente para decidir si un dispositivo debe recibir imágenes de alta o baja calidad. Además, no hace falta decir que todo lo relacionado con el usuario-agente es un hackeo y, si es posible, se debe evitar.

Usa consultas de medios de CSS

Dado que son declarativas, las consultas de medios CSS te permiten indicar tu intención y dejar que el navegador haga lo correcto por ti. Además del uso más común de las consultas de medios (coincidencia de tamaño del dispositivo), también puedes hacer coincidir devicePixelRatio. La consulta de medios asociada es device-pixel-ratio y tiene variantes mínimas y máximas asociadas, como es de esperarse. Si quieres cargar imágenes de alta resolución y la relación de píxeles del dispositivo supera un umbral, puedes hacer lo siguiente:

#my-image { background: (low.png); }

@media only screen and (min-device-pixel-ratio: 1.5) {
  #my-image { background: (high.png); }
}

Se vuelve un poco más complicado con todos los prefijos de proveedores mezclados, en especial debido a las diferencias en la ubicación de los prefijos “min” y “max”:

@media only screen and (min--moz-device-pixel-ratio: 1.5),
    (-o-min-device-pixel-ratio: 3/2),
    (-webkit-min-device-pixel-ratio: 1.5),
    (min-device-pixel-ratio: 1.5) {

  #my-image {
    background:url(high.png);
  }
}

Con este enfoque, recuperas los beneficios del análisis anticipado, que se perdieron con la solución de JS. También obtienes la flexibilidad de elegir tus puntos de inflexión responsivos (por ejemplo, puedes tener imágenes de DPI bajas, medias y altas), que se perdían con el enfoque del servidor.

Lamentablemente, sigue siendo un poco difícil de manejar y genera una CSS de aspecto extraño (o requiere procesamiento previo). Además, este enfoque se restringe a las propiedades CSS, por lo que no hay forma de establecer un <img src>, y todas tus imágenes deben ser elementos con un fondo. Por último, si dependes estrictamente de la relación de píxeles del dispositivo, puedes terminar en situaciones en las que tu teléfono inteligente de alta densidad de píxeles termine descargando un recurso de imagen de 2 veces el tamaño mientras está en una conexión EDGE. Esta no es la mejor experiencia del usuario.

Usar las nuevas funciones del navegador

Recientemente, se ha debatido mucho sobre la compatibilidad de las plataformas web con el problema de las imágenes de alta resolución. Hace poco, Apple irrumpió en el espacio y llevó la función de CSS image-set() a WebKit. Como resultado, tanto Safari como Chrome lo admiten. Como es una función de CSS, image-set() no aborda el problema de las etiquetas <img>. Ingresa @srcset, que aborda este problema, pero (en el momento de escribir este artículo) aún no tiene implementaciones de referencia. En la siguiente sección, se profundiza en image-set y srcset.

Funciones del navegador para una alta compatibilidad con valores de DPI

En última instancia, la decisión sobre qué enfoque adoptar depende de tus requisitos particulares. Dicho esto, ten en cuenta que todos los enfoques mencionados anteriormente tienen inconvenientes. Sin embargo, una vez que image-set y srcset sean compatibles con muchos dispositivos, serán las soluciones adecuadas para este problema. Por ahora, hablemos sobre algunas prácticas recomendadas que pueden acercarnos lo más posible a ese futuro ideal.

En primer lugar, ¿en qué se diferencian? Bueno, image-set() es una función de CSS, adecuada para usarse como valor de la propiedad CSS de fondo. srcset es un atributo específico de los elementos <img>, con una sintaxis similar. Ambas etiquetas te permiten especificar declaraciones de imágenes, pero el atributo srcset también te permite configurar qué imagen cargar en función del tamaño del viewport.

Prácticas recomendadas para el conjunto de imágenes

La función CSS image-set() está disponible con el prefijo -webkit-image-set(). La sintaxis es bastante simple y toma una o más declaraciones de imagen separadas por comas, que consisten en una cadena de URL o una función url() seguida de la resolución asociada. Por ejemplo:

background-image:  -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

Esto le indica al navegador que hay dos imágenes para elegir. Una de ellas está optimizada para pantallas 1x y la otra para pantallas 2x. Luego, el navegador puede elegir cuál cargar en función de una variedad de factores, que incluso pueden incluir la velocidad de la red, si el navegador es lo suficientemente inteligente (hasta donde sé, no se implementó actualmente).

Además de cargar la imagen correcta, el navegador también la adaptará de manera adecuada. En otras palabras, el navegador supone que 2 imágenes son el doble de grandes que las imágenes de 1x, por lo que reducirá la escala de la imagen de 2x por un factor de 2 para que la imagen parezca tener el mismo tamaño en la página.

En lugar de especificar 1x, 1.5x o Nx, también puedes especificar una densidad de píxeles del dispositivo en dpi.

Esto funciona bien, excepto en los navegadores que no admiten la propiedad image-set, que no mostrará ninguna imagen. Esto es claramente malo, por lo que debes usar un resguardo (o una serie de resguardos) para abordar ese problema:

background-image: url(icon1x.jpg);
background-image: -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);
/* This will be useful if image-set gets into the platform, unprefixed.
    Also include other prefixed versions of this */
background-image: image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

La imagen anterior cargará el recurso correspondiente en navegadores compatibles con image-set y, de lo contrario, regresará al recurso 1x. La advertencia predecible es que, si bien la compatibilidad con navegadores de image-set() es baja, la mayoría de los usuarios-agentes obtendrán el recurso 1x.

En esta demostración, se usa image-set() para cargar la imagen correcta y se recurre al recurso 1x si no se admite esta función de CSS.

En este punto, es posible que te preguntes por qué no solo se usa un polyfill (es decir, se crea un complemento de JavaScript para) image-set() y se da por terminado el día. Resulta que es bastante difícil implementar polyfills eficientes para las funciones de CSS. (para obtener una explicación detallada del motivo, consulta esta discusión de estilo www).

Archivo srcset de la imagen

Este es un ejemplo de srcset:

<img alt="my awesome image"
  src="banner.jpeg"
  srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">

Como puedes ver, además de las declaraciones de x que proporciona image-set, el elemento srcset también toma valores de w y h que corresponden al tamaño del viewport y, de esta manera, intenta entregar la versión más relevante. Lo anterior entregaría banner-phone.jpeg a dispositivos con un ancho de ventana de visualización inferior a 640 px, banner-phone-HD.jpeg a dispositivos de alta densidad de píxeles con pantallas pequeñas, banner-HD.jpeg a dispositivos de alta densidad de píxeles con pantallas superiores a 640 px y banner.jpeg a todo lo demás.

Cómo usar image-set para elementos de imagen

Debido a que el atributo srcset en los elementos img no está implementado en la mayoría de los navegadores, puede ser tentador reemplazar tus elementos img por <div> con fondos y usar el enfoque de conjunto de imágenes. Esto funcionará, con advertencias. El inconveniente aquí es que la etiqueta <img> tiene un valor semántico a largo plazo. En la práctica, esto es importante principalmente por motivos de accesibilidad y para los rastreadores web.

Si terminas usando -webkit-image-set, es posible que te sientas tentado a usar la propiedad de CSS en segundo plano. La desventaja de este enfoque es que debes especificar el tamaño de la imagen, que es desconocido si usas una imagen que no es de 1x. En lugar de hacerlo, puedes usar la propiedad CSS de contenido de la siguiente manera:

<div id="my-content-image"
  style="content: -webkit-image-set(
    url(icon1x.jpg) 1x,
    url(icon2x.jpg) 2x);">
</div>

Esto escalará automáticamente la imagen según devicePixelRatio. Consulta este ejemplo de la técnica anterior en acción, con un resguardo adicional de url() para los navegadores que no admiten image-set.

Cómo usar el polyfill de srcset

Una función útil de srcset es que incluye un resguardo natural. En el caso de que no se implemente el atributo srcset, todos los navegadores saben procesar el atributo src. Además, como es solo un atributo HTML, es posible crear polyfills con JavaScript.

Este polyfill incluye pruebas de unidades para garantizar que esté lo más cerca posible de la especificación. Además, existen verificaciones que impiden que el polyfill ejecute cualquier código si srcset se implementa de forma nativa.

Aquí tienes una demostración del polyfill en acción.

Conclusión

No hay una solución mágica para resolver el problema de las imágenes de alta DPI.

La solución más sencilla es evitar las imágenes por completo y optar por SVG y CSS. Sin embargo, esto no siempre es realista, especialmente si tienes imágenes de alta calidad en tu sitio.

Los enfoques en JS, CSS y el uso del servidor tienen sus fortalezas y debilidades. Sin embargo, el enfoque más prometedor es aprovechar las nuevas funciones del navegador. Si bien la compatibilidad del navegador con image-set y srcset aún está incompleta, existen resguardos razonables para usar en la actualidad.

En resumen, mis recomendaciones son las siguientes:

  • Para las imágenes de fondo, usa image-set con los resguardos adecuados para los navegadores que no lo admiten.
  • Para las imágenes de contenido, usa un polyfill de srcset o recurre a usar image-set (consulta más arriba).
  • En situaciones en las que estés dispuesto a sacrificar la calidad de la imagen, considera usar imágenes 2 veces comprimidas.