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

Una de las funciones del complejo entorno horizontal actual es que existe una amplia variedad de densidades de píxeles de la pantalla. Algunos dispositivos cuentan con pantallas de muy alta resolución, mientras que otros se retrasan. Los desarrolladores de aplicaciones deben admitir una variedad de densidades de píxeles, lo cual puede ser un desafío. En la Web móvil, los desafíos se deben agravar por varios factores, como los siguientes:

  • 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 entregar 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 hacer esto hoy y en un futuro cercano.

Si es posible, evita las imágenes

Antes de abrir esta lata de gusanos, recuerda que la Web tiene muchas tecnologías potentes que son, en gran medida, independientes de la resolución y de los DPI. Específicamente, el texto, SVG y gran parte de CSS "funcionarán" gracias a la función de ajuste de escala automático de píxeles de la Web (a través de devicePixelRatio).

Dicho esto, no siempre puedes evitar las imágenes de trama. Por ejemplo, es posible que se te proporcionen recursos que serían bastante difíciles de replicar en SVG/CSS puros, o bien trabajarías con una fotografía. Si bien puedes convertir la imagen a SVG automáticamente, la vectorización de fotografías no tiene mucho sentido, ya que las versiones ampliadas no suelen verse bien.

Información general

Una breve historia de la densidad de visualización

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

La densidad de píxeles de las pantallas mejoró gradualmente, en gran medida debido al caso de uso de dispositivos móviles, en el que los usuarios suelen acercar los teléfonos a la cara para que los píxeles sean más visibles. En 2008, los teléfonos de 150 dpi eran la nueva norma. La tendencia en aumento en la densidad de la pantalla continuó, y los teléfonos nuevos tienen pantallas de 300 dpi (con la marca "Retina" de Apple).

El santo grial, por supuesto, es una pantalla en la que los píxeles son completamente invisibles. En cuanto al factor de forma del teléfono, la generación actual de pantallas Retina/HiDPI puede parecerse mucho a ese ideal. Sin embargo, es probable que las nuevas clases de hardware y wearables, como Project Glass, sigan impulsando una mayor densidad de píxeles.

En la práctica, las imágenes de baja densidad deberían verse igual en pantallas nuevas que en las antiguas, 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 desagradables 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 de 2x se ve bastante bien.

Babuón 1x
Babuón 2x
¡Bombones! en diferentes densidades de píxeles.

Píxeles en la Web

Cuando se diseñó la Web, el 99% de las pantallas tenían 96 dpi (o se pretendía ser), y se hicieron algunas disposiciones para la variación en este aspecto. Debido a una 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.

Recientemente, la especificación de HTML abordó este problema definiendo un píxel de referencia que los fabricantes usan para determinar el tamaño de un píxel de 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 smartphone tiene una pantalla con un tamaño de píxeles físico de 180 píxeles por pulgada (PPP). Para calcular la proporción de píxeles del dispositivo, se requieren tres pasos:

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

    Según la especificación, sabemos que, con 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 de una laptop. Estimemos que esa distancia será de 18 pulgadas.

  2. Multiplica la relación de distancia con la densidad estándar (96 ppp) 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ísicos 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.
Un diagrama que muestra un píxel angular de referencia para ayudar a ilustrar cómo se calcula devicePixelRatio.

Por lo tanto, 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 hace referencia a la proporción de píxeles del dispositivo de 1.2, es decir, para cada píxel ideal, este dispositivo tiene 1.2 píxeles físicos. Esta es la fórmula para elegir entre los píxeles ideales (como se define en las especificaciones web) y los píxeles físicos (puntos en la pantalla del dispositivo):

physicalPixels = window.devicePixelRatio * idealPixels

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

La unidad de píxeles se refiere a la cantidad total de píxeles del dispositivo que se acercan mejor al píxel de referencia.

Un motivo por el que las proporciones redondeadas pueden ser mejores es porque pueden generar menos artefactos de subpíxeles.

Sin embargo, la realidad del panorama de los dispositivos es mucho más variado y los teléfonos Android a menudo tienen DPR de 1.5. La tablet Nexus 7 tiene una DPR de aproximadamente 1.33, que se obtuvo con un cálculo similar al anterior. En el futuro habrá más dispositivos con DPR variables. Por lo tanto, nunca debes suponer que tus clientes tendrán DPR enteros.

Descripción general de las técnicas de imagen de HiDPI

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

  1. La optimización de imágenes individuales
  2. Optimiza la selección entre varias imágenes.

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

  • Imagen de HiDPI muy comprimida
  • Formato de imagen increíble
  • Formato de imagen progresivo

Enfoques con 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, determine una estrategia de decisión. Estas son las opciones que aparecen:

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

Imagen de HiDPI muy comprimida

Las imágenes ya componen el impresionante 60% del ancho de banda que se dedica a descargar un sitio web promedio. Si entregamos imágenes de HiDPI a todos los clientes, aumentaremos esta cantidad. ¿Cuánto crecerá?

Realicé algunas pruebas que generaron fragmentos de imágenes de 1x y 2x con calidad JPEG a 90, 50 y 20. Esta es la secuencia de comandos de shell que usé (utilizando 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 compensación entre calidad y tamaño. Para mí, las imágenes de 2x muy comprimidas en realidad se ven mejor que las imágenes de 1x sin comprimir.

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

La comparación anterior se hizo completamente con JPEG comprimidos. Vale la pena señalar que existen muchas compensaciones entre los formatos de imagen ampliamente implementados (JPEG, PNG, GIF), lo que nos lleva a...

Formato de imagen increíble

WebP es un formato de imagen atractivo que se comprime muy bien y mantiene una alta fidelidad de las imágenes. Por supuesto, todavía no se implementó en todas partes.

Una forma es comprobar la compatibilidad con WebP es a través de JavaScript. Cargas una imagen de 1 px a través de data-uri, esperas a que se carguen los eventos cargados o se activen los errores 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 mediante la función image(). Por lo tanto, si tienes una imagen WebP y un resguardo de JPEG, puedes escribir lo siguiente:

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

Este enfoque tiene algunos problemas. En primer lugar, image() no se implementó de forma general. En segundo lugar, si bien la compresión de WebP lleva los JPEG fuera del agua, sigue siendo una mejora relativamente incremental: aproximadamente un 30% más pequeña según esta galería WebP. Por lo tanto, usar WebP por sí solo no es suficiente para abordar el problema de los DPI.

Formatos de imagen progresivos

Los formatos de imagen progresivos, como JPEG 2000, JPEG progresivo, PNG progresivo y GIF, tienen el beneficio (algo debatido) de ver la imagen en su lugar antes de que se cargue por completo. Pueden generar una sobrecarga de tamaño considerable, aunque hay evidencia contradictoria sobre esto. Jeff Atwood afirmó que el modo progresivo "agrega aproximadamente un 20% al tamaño de las imágenes PNG y aproximadamente un 10% al tamaño de las imágenes JPEG y GIF". Sin embargo, Stoyan Stefanov afirmaba que, para 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 una entrega de imágenes de 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 detecte que los datos adicionales no aumentarán la calidad de la imagen (es decir, todas las mejoras de fidelidad serán subpíxeles).

Si bien las conexiones son fáciles de finalizar, a menudo es costoso reiniciarlas. Para 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 interrumpe de forma prematura, debido a que una imagen se descargó lo suficiente, el navegador deberá crear una conexión nueva, que puede ser muy lenta en entornos de baja latencia.

Una solución alternativa es usar la solicitud HTTP Range, que permite a los navegadores especificar un rango de bytes para recuperar. Un navegador inteligente podría hacer una solicitud HEAD para llegar al encabezado, procesarlo, decidir qué cantidad de la imagen se necesita realmente y, luego, recuperarlo. Por desgracia, el rango de HTTP no es compatible con los servidores web, por lo que este enfoque no es práctico.

Por último, una limitación obvia de este enfoque es que no puedes elegir qué imagen cargar, sino que solo varía la fidelidad de la misma imagen. Como resultado, esto no aborda el caso de uso de "dirección artística".

Usa JavaScript para decidir qué imagen cargar

El primer enfoque, y el más evidente, para decidir qué imagen cargar es usar JavaScript en el cliente. Este enfoque te permite obtener toda la información 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 detectar la conexión de red con 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 realizan algo similar a 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 la imagen hasta que el analizador de lectura anticipada haya terminado. En esencia, esto significa que las imágenes no comenzarán a descargarse hasta después de que se active el evento pageload. Obtén más información en este artículo de Jason Grigsby.

Decide qué imagen cargar en el servidor

Puedes diferir la decisión al servidor; para ello, escribe controladores de solicitudes personalizados para cada imagen que entregues. Este controlador verificará la compatibilidad con Retina en función del usuario-agente (la única información que se retransmite al servidor). Luego, según si la lógica del servidor desea entregar elementos de HiDPI, debes cargar el recurso correspondiente (nombrado según alguna convención conocida).

Lamentablemente, el usuario-agente no necesariamente proporciona suficiente información para decidir si un dispositivo debe recibir imágenes de alta o baja calidad. Además, no hace falta decir que cualquier cosa relacionada con el usuario-agente es un hackeo y debe evitarse siempre que sea posible.

Usar consultas de medios de CSS

Dado que son declarativas, las consultas de medios de 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 consultas de medios (coincidencia con el tamaño del dispositivo), también puedes hacer coincidir devicePixelRatio. La consulta de medios asociada es la proporción dispositivo-píxeles, y tiene variantes mínimas y máximas asociadas, como es de esperarse. Si deseas cargar imágenes con valores altos de DPI y la proporció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 si se mezclan todos los prefijos de proveedores, en especial debido a las diferencias en la posició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 perdió con la solución JS. También obtienes la flexibilidad de elegir los puntos de interrupción responsivos (por ejemplo, puedes tener imágenes con DPI bajos, medios y altos), lo que se perdió con el enfoque del servidor.

Lamentablemente, sigue siendo un poco difícil de manejar y genera una CSS de apariencia extraña (o requiere procesamiento previo). Además, este enfoque se restringe a las propiedades de CSS, por lo que no hay forma de establecer un <img src>, y tus imágenes deben ser elementos con un fondo. Por último, si te basas estrictamente en la proporción de píxeles del dispositivo, puedes terminar en situaciones en las que tu teléfono inteligente con un valor de DPI alto descarga un recurso de imagen 2x masivo mientras tiene una conexión EDGE. Esta no es la mejor experiencia del usuario.

Usa las nuevas funciones del navegador

Hubo muchos debates recientes sobre la compatibilidad de las plataformas web con el problema de las imágenes con valores altos de DPI. Apple irrumpió en el espacio recientemente y llevó la función de CSS image-set() a WebKit. Como resultado, Safari y Chrome lo admiten. Como se trata de una función de CSS, image-set() no soluciona el problema de las etiquetas <img>. Ingresa @srcset, que soluciona este problema pero (al momento de redactar este documento), no tiene implementaciones de referencia (todavía). En la siguiente sección, se profundiza en image-set y srcset.

Funciones del navegador para la compatibilidad con niveles altos de DPI

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

En primer lugar, ¿en qué se diferencian estos dos? image-set() es una función de CSS, adecuada para usarse como valor de la propiedad en segundo plano de CSS. 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 según el tamaño del viewport.

Prácticas recomendadas para el conjunto de imágenes

La función de CSS image-set() está disponible con el prefijo -webkit-image-set(). La sintaxis es bastante simple: 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 está optimizada para pantallas de 1x y la otra para pantallas de 2x. Luego, el navegador puede elegir cuál cargar en función de diversos factores, que incluso pueden incluir la velocidad de la red, si el navegador es lo suficientemente inteligente (por el momento, no está implementado hasta donde sé).

Además de cargar la imagen correcta, el navegador también la escalará según corresponda. En otras palabras, el navegador supone que 2 imágenes son el doble de grandes que las imágenes 1x y, por lo tanto, reduce la escala de la imagen 2x en un factor de 2, de modo 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 determinada densidad de píxeles del dispositivo en dpi.

Esto funciona bien, excepto en navegadores que no admiten la propiedad image-set, en la que no se 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
);

Lo anterior cargará el recurso adecuado en navegadores compatibles con image-set. De lo contrario, recurrirá al recurso 1x. La salvedad obvia es que, si bien la compatibilidad con el navegador image-set() es baja, la mayoría de los usuarios-agentes obtendrán el recurso 1x.

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

En este punto, es posible que te preguntes por qué no usar polyfill (es decir, compilar una corrección de compatibilidad de JavaScript para) image-set() y finalizar el día. Resulta que es bastante difícil implementar polyfills eficientes para funciones de CSS. (para obtener una explicación detallada del motivo, consulta esta discusión sobre www-style).

Imagen srcset

A continuación, se muestra 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 x que proporciona image-set, el elemento srcset también toma valores w y h que corresponden al tamaño del viewport para intentar entregar la versión más relevante. El ejemplo anterior mostraría banner-phone.jpeg a dispositivos con un ancho de viewport inferior a 640 px, banner-phone-HD.jpeg a dispositivos de pantalla pequeña con altos DPI, banner-HD.jpeg a dispositivos de alto DPI con pantallas de más de 640 px y banner.jpeg a todo lo demás.

Usa image-set para elementos de imagen

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

Si terminas usando -webkit-image-set, es posible que quieras usar la propiedad de CSS en segundo plano. La desventaja de este enfoque es que debes especificar el tamaño de la imagen, lo cual se desconoce si usas una imagen que no es de 1x. En lugar de hacer esto, puedes usar la propiedad de contenido de CSS de la siguiente manera:

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

De esta manera, se ajustará automáticamente la escala de la imagen según el devicePixelRatio. Consulta este ejemplo de la técnica anterior en acción, con un resguardo adicional de url() para navegadores que no admiten image-set.

Conjunto de polifills en srcset

Una función práctica de srcset es que viene con un resguardo natural. En caso de que el atributo srcset no se implemente, todos los navegadores sabrán que deben procesar el atributo src. Además, como es solo un atributo HTML, puedes crear polyfills con JavaScript.

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

Esta es una demostración del polyfill en acción.

Conclusión

No existe una solución mágica para resolver el problema de las imágenes con valores altos de DPI.

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

Los enfoques en JS, CSS y el uso del servidor tienen sus ventajas y desventajas. No obstante, el enfoque más prometedor es aprovechar las funciones nuevas del navegador. Si bien la compatibilidad del navegador con image-set y srcset todavía no está completa, hay resguardos razonables para usar hoy en día.

Para resumir, mis recomendaciones son las siguientes:

  • En el caso de las imágenes de fondo, usa image-set con los resguardos correspondientes para los navegadores que no lo admitan.
  • Para imágenes de contenido, usa un polyfill srcset, o el resguardo para usar image-set (ver arriba).
  • En las situaciones en las que estés dispuesto a sacrificar la calidad de la imagen, considera usar imágenes de 2x muy comprimidas.