Carga de imágenes más rápida con vistas previas de imágenes integradas
Publicado: 2022-03-10La vista previa de imagen de baja calidad (LQIP) y la variante SQIP basada en SVG son las dos técnicas predominantes para la carga diferida de imágenes. Lo que ambos tienen en común es que primero genera una imagen de vista previa de baja calidad. Esto se mostrará borroso y luego se reemplazará por la imagen original. ¿Qué pasaría si pudiera presentar una imagen de vista previa al visitante del sitio web sin tener que cargar datos adicionales?
Los archivos JPEG, para los que se utiliza principalmente la carga diferida, tienen la posibilidad, según la especificación, de almacenar los datos contenidos en ellos de tal manera que primero se muestren los contenidos de imagen gruesos y luego los detallados. En lugar de que la imagen se construya de arriba a abajo durante la carga (modo de línea de base), se puede mostrar una imagen borrosa muy rápidamente, que gradualmente se vuelve más y más nítida (modo progresivo).
Además de la mejor experiencia de usuario proporcionada por la apariencia que se muestra más rápidamente, los archivos JPEG progresivos también suelen ser más pequeños que sus contrapartes codificadas de línea de base. Para archivos de más de 10 kB, existe una probabilidad del 94 por ciento de obtener una imagen más pequeña cuando se usa el modo progresivo, según Stoyan Stefanov del equipo de desarrollo de Yahoo.
Si su sitio web consta de muchos archivos JPEG, notará que incluso los archivos JPEG progresivos se cargan uno tras otro. Esto se debe a que los navegadores modernos solo permiten seis conexiones simultáneas a un dominio. Por lo tanto, los archivos JPEG progresivos por sí solos no son la solución para dar al usuario la impresión más rápida posible de la página. En el peor de los casos, el navegador cargará una imagen por completo antes de comenzar a cargar la siguiente.
La idea que se presenta aquí ahora es cargar solo tantos bytes de un JPEG progresivo desde el servidor que pueda obtener rápidamente una impresión del contenido de la imagen. Más tarde, en un momento definido por nosotros (por ejemplo, cuando se hayan cargado todas las imágenes de vista previa en la ventana gráfica actual), el resto de la imagen debe cargarse sin volver a solicitar la parte ya solicitada para la vista previa.
Desafortunadamente, no puede decirle a una etiqueta img
en un atributo qué parte de la imagen debe cargarse en qué momento. Sin embargo, con Ajax, esto es posible, siempre que el servidor que entrega la imagen admita solicitudes de rango HTTP.
Usando solicitudes de rango HTTP, un cliente puede informar al servidor en un encabezado de solicitud HTTP qué bytes del archivo solicitado se incluirán en la respuesta HTTP. Esta característica, compatible con cada uno de los servidores más grandes (Apache, IIS, nginx), se usa principalmente para la reproducción de video. Si un usuario salta al final de un video, no sería muy eficiente cargar el video completo antes de que el usuario finalmente pueda ver la parte deseada. Por lo tanto, el servidor solo solicita los datos de video alrededor del tiempo solicitado por el usuario, para que el usuario pueda ver el video lo más rápido posible.
Ahora nos enfrentamos a los siguientes tres desafíos:
- Crear el JPEG progresivo
- Determinar el desplazamiento de bytes hasta el cual la primera solicitud de rango HTTP debe cargar la imagen de vista previa
- Creación del código JavaScript de la interfaz
1. Creando el JPEG progresivo
Un JPEG progresivo consiste en varios de los llamados segmentos de escaneo, cada uno de los cuales contiene una parte de la imagen final. El primer escaneo muestra la imagen solo de manera muy aproximada, mientras que los siguientes en el archivo agregan información cada vez más detallada a los datos ya cargados y finalmente forman la apariencia final.
El aspecto exacto de los escaneos individuales lo determina el programa que genera los archivos JPEG. En programas de línea de comandos como cjpeg del proyecto mozjpeg, incluso puede definir qué datos contienen estos escaneos. Sin embargo, esto requiere un conocimiento más profundo, que iría más allá del alcance de este artículo. Para esto, me gustaría referirme a mi artículo "Comprender finalmente JPG", que enseña los conceptos básicos de la compresión JPEG. Los parámetros exactos que deben pasarse al programa en un script de escaneo se explican en el archivo Wizard.txt del proyecto mozjpeg. En mi opinión, los parámetros del script de escaneo (siete escaneos) usados por mozjpeg por defecto son un buen compromiso entre la estructura progresiva rápida y el tamaño del archivo y, por lo tanto, pueden adoptarse.
Para transformar nuestro JPEG inicial en un JPEG progresivo, usamos jpegtran
del proyecto mozjpeg. Esta es una herramienta para realizar cambios sin pérdidas en un archivo JPEG existente. Las compilaciones precompiladas para Windows y Linux están disponibles aquí: https://mozjpeg.codelove.de/binaries.html. Si prefiere ir a lo seguro por razones de seguridad, es mejor construirlos usted mismo.
Desde la línea de comandos ahora creamos nuestro JPEG progresivo:
$ jpegtran input.jpg > progressive.jpg
jpegtran asume el hecho de que queremos construir un JPEG progresivo y no es necesario especificarlo explícitamente. Los datos de la imagen no se cambiarán de ninguna manera. Solo se cambia la disposición de los datos de la imagen dentro del archivo.
Idealmente, los metadatos irrelevantes para la apariencia de la imagen (como los datos Exif, IPTC o XMP) deberían eliminarse del JPEG, ya que los decodificadores de metadatos solo pueden leer los segmentos correspondientes si preceden al contenido de la imagen. Dado que no podemos moverlos detrás de los datos de la imagen en el archivo por este motivo, ya se entregarían con la imagen de vista previa y ampliarían la primera solicitud en consecuencia. Con el programa de línea de comandos exiftool
puede eliminar fácilmente estos metadatos:
$ exiftool -all= progressive.jpg
Si no desea utilizar una herramienta de línea de comandos, también puede utilizar el servicio de compresión en línea compress-or-die.com para generar un JPEG progresivo sin metadatos.
2. Determine el desplazamiento de bytes hasta el cual la primera solicitud de rango HTTP debe cargar la imagen de vista previa
Un archivo JPEG se divide en diferentes segmentos, cada uno de los cuales contiene diferentes componentes (datos de imagen, metadatos como IPTC, Exif y XMP, perfiles de color incrustados, tablas de cuantificación, etc.). Cada uno de estos segmentos comienza con un marcador introducido por un byte FF
hexadecimal. A esto le sigue un byte que indica el tipo de segmento. Por ejemplo, D8
completa el marcador hasta el marcador SOI FF D8
(Start Of Image), con el que comienza cada archivo JPEG.
Cada inicio de un escaneo está marcado por el marcador SOS (Start Of Scan, hexadecimal FF DA
). Dado que los datos detrás del marcador SOS están codificados por entropía (los JPEG usan la codificación Huffman), hay otro segmento con las tablas Huffman (DHT, hexadecimal FF C4
) necesarios para la decodificación antes del segmento SOS. El área de interés para nosotros dentro de un archivo JPEG progresivo, por lo tanto, consiste en alternar tablas de Huffman/segmentos de datos de escaneo. Por lo tanto, si queremos mostrar el primer escaneo muy aproximado de una imagen, debemos solicitar todos los bytes hasta la segunda aparición de un segmento DHT (hexadecimal FF C4
) del servidor.
En PHP, podemos usar el siguiente código para leer la cantidad de bytes necesarios para todos los escaneos en una matriz:
<?php $img = "progressive.jpg"; $jpgdata = file_get_contents($img); $positions = []; $offset = 0; while ($pos = strpos($jpgdata, "\xFF\xC4", $offset)) { $positions[] = $pos+2; $offset = $pos+2; }
Tenemos que agregar el valor de dos a la posición encontrada porque el navegador solo muestra la última fila de la imagen de vista previa cuando encuentra un nuevo marcador (que consta de dos bytes, como se acaba de mencionar).
Dado que estamos interesados en la primera imagen de vista previa en este ejemplo, encontramos la posición correcta en $positions[1]
hasta la cual tenemos que solicitar el archivo a través de HTTP Range Request. Para solicitar una imagen con una mejor resolución, podríamos usar una posición posterior en la matriz, por ejemplo, $positions[3]
.
3. Crear el código JavaScript de la interfaz
En primer lugar, definimos una etiqueta img
, a la que le damos la posición del byte recién evaluado:
<img data-src="progressive.jpg" data-bytes="<?= $positions[1] ?>">
Como suele ser el caso con las bibliotecas de carga diferida, no definimos el atributo src
directamente para que el navegador no comience a solicitar inmediatamente la imagen del servidor al analizar el código HTML.
Con el siguiente código JavaScript ahora cargamos la imagen de vista previa:
var $img = document.querySelector("img[data-src]"); var URL = window.URL || window.webkitURL; var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ $img.src_part = this.response; $img.src = URL.createObjectURL(this.response); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes=0-" + $img.getAttribute('data-bytes')); xhr.responseType = 'blob'; xhr.send();
Este código crea una solicitud Ajax que le dice al servidor en un encabezado de rango HTTP que devuelva el archivo desde el principio hasta la posición especificada en data-bytes
... y nada más. Si el servidor comprende las solicitudes de rango HTTP, devuelve los datos de la imagen binaria en una respuesta HTTP-206 (HTTP 206 = contenido parcial) en forma de blob, a partir del cual podemos generar una URL interna del navegador mediante createObjectURL
. Usamos esta URL como src
para nuestra etiqueta img
. Así hemos cargado nuestra imagen de vista previa.
Almacenamos el blob adicionalmente en el objeto DOM en la propiedad src_part
, porque necesitaremos estos datos de inmediato.
En la pestaña de red de la consola del desarrollador puedes comprobar que no hemos cargado la imagen completa, sino solo una pequeña parte. Además, la URL de carga del blob debe mostrarse con un tamaño de 0 bytes.
Dado que ya cargamos el encabezado JPEG del archivo original, la imagen de vista previa tiene el tamaño correcto. Así, dependiendo de la aplicación, podemos omitir el alto y el ancho de la etiqueta img
.
Alternativa: cargar la imagen de vista previa en línea
Por razones de rendimiento, también es posible transferir los datos de la imagen de vista previa como URI de datos directamente en el código fuente HTML. Esto nos ahorra la sobrecarga de transferir los encabezados HTTP, pero la codificación base64 hace que los datos de la imagen sean un tercio más grandes. Esto se relativiza si entrega el código HTML con una codificación de contenido como gzip o brotli , pero aún debe usar URI de datos para imágenes de vista previa pequeñas.
Mucho más importante es el hecho de que las imágenes de vista previa están disponibles de inmediato y no hay un retraso perceptible para el usuario al construir la página.
En primer lugar, tenemos que crear el URI de datos, que luego usamos en la etiqueta img
como src
. Para esto, creamos el URI de datos a través de PHP, por lo que este código se basa en el código recién creado, que determina las compensaciones de bytes de los marcadores SOS:
<?php … $fp = fopen($img, 'r'); $data_uri = 'data:image/jpeg;base64,'. base64_encode(fread($fp, $positions[1])); fclose($fp);
El URI de datos creado ahora se inserta directamente en la etiqueta `img` como src
:
<img src="<?= $data_uri ?>" data-src="progressive.jpg" alt="">
Por supuesto, el código JavaScript también debe adaptarse:
<script> var $img = document.querySelector("img[data-src]"); var binary = atob($img.src.slice(23)); var n = binary.length; var view = new Uint8Array(n); while(n--) { view[n] = binary.charCodeAt(n); } $img.src_part = new Blob([view], { type: 'image/jpeg' }); $img.setAttribute('data-bytes', $img.src_part.size - 1); </script>
En lugar de solicitar los datos a través de una solicitud de Ajax, donde recibiríamos inmediatamente un blob, en este caso tenemos que crear el blob nosotros mismos a partir de la URI de datos. Para ello, liberamos el data-URI de la parte que no contiene datos de imagen: data:image/jpeg;base64
. Decodificamos los datos codificados en base64 restantes con el comando atob
. Para crear un blob a partir de los datos de cadena ahora binarios, tenemos que transferir los datos a una matriz Uint8, lo que garantiza que los datos no se traten como un texto codificado en UTF-8. A partir de esta matriz, ahora podemos crear un blob binario con los datos de imagen de la imagen de vista previa.
Para que no tengamos que adaptar el siguiente código para esta versión en línea, agregamos el atributo data-bytes
en la etiqueta img
, que en el ejemplo anterior contiene el desplazamiento de bytes desde el cual se debe cargar la segunda parte de la imagen. .
En la pestaña de red de la consola del desarrollador, también puede verificar aquí que cargar la imagen de vista previa no genera una solicitud adicional, mientras que el tamaño del archivo de la página HTML ha aumentado.
Cargando la imagen final
En un segundo paso cargamos el resto del archivo de imagen después de dos segundos como ejemplo:
setTimeout(function(){ var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ var blob = new Blob([$img.src_part, this.response], { type: 'image/jpeg'} ); $img.src = URL.createObjectURL(blob); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes="+ (parseInt($img.getAttribute('data-bytes'), 10)+1) +'-'); xhr.responseType = 'blob'; xhr.send(); }, 2000);
En el encabezado Rango esta vez especificamos que queremos solicitar la imagen desde la posición final de la imagen de vista previa hasta el final del archivo. La respuesta a la primera solicitud se almacena en la propiedad src_part
del objeto DOM. Usamos las respuestas de ambas solicitudes para crear un nuevo blob por new Blob()
, que contiene los datos de toda la imagen. La URL del blob generada a partir de esto se usa nuevamente como src
del objeto DOM. Ahora la imagen está completamente cargada.
Además, ahora podemos verificar nuevamente los tamaños cargados en la pestaña de red de la consola del desarrollador.
Prototipo
En la siguiente URL, proporcioné un prototipo donde puede experimentar con diferentes parámetros: https://embedded-image-preview.cerdmann.com/prototype/
El repositorio de GitHub para el prototipo se puede encontrar aquí: https://github.com/McSodbrenner/embedded-image-preview
Consideraciones al final
Usando la tecnología de vista previa de imagen integrada (EIP) que se presenta aquí, podemos cargar imágenes de vista previa cualitativamente diferentes de archivos JPEG progresivos con la ayuda de Ajax y HTTP Range Requests. Los datos de estas imágenes de vista previa no se descartan, sino que se reutilizan para mostrar la imagen completa.
Además, no es necesario crear imágenes de vista previa. En el lado del servidor, solo se debe determinar y guardar el desplazamiento de bytes en el que finaliza la imagen de vista previa. En un sistema CMS, debería ser posible guardar este número como un atributo en una imagen y tenerlo en cuenta al mostrarlo en la etiqueta img
. Incluso sería concebible un flujo de trabajo que complemente el nombre de archivo de la imagen con el desplazamiento, por ejemplo, progresivo-8343.jpg , para no tener que guardar el desplazamiento aparte del archivo de imagen. Este desplazamiento podría ser extraído por el código JavaScript.
Dado que los datos de la imagen de vista previa se reutilizan, esta técnica podría ser una mejor alternativa al enfoque habitual de cargar una imagen de vista previa y luego un WebP (y proporcionar un respaldo de JPEG para los navegadores que no son compatibles con WebP). La imagen de vista previa a menudo destruye las ventajas de almacenamiento de WebP, que no admite el modo progresivo.
Actualmente, las imágenes de vista previa en LQIP normal son de calidad inferior, ya que se supone que cargar los datos de vista previa requiere ancho de banda adicional. Como ya dejó claro Robin Osborne en una entrada de blog en 2018, no tiene mucho sentido mostrar marcadores de posición que no den una idea de la imagen final. Mediante el uso de la técnica sugerida aquí, podemos mostrar un poco más de la imagen final como una imagen de vista previa sin dudarlo presentando al usuario un escaneo posterior del JPEG progresivo.
En caso de una conexión de red débil del usuario, puede tener sentido, dependiendo de la aplicación, no cargar todo el archivo JPEG, sino, por ejemplo, omitir los dos últimos escaneos. Esto produce un JPEG mucho más pequeño con una calidad ligeramente reducida. El usuario nos lo agradecerá y no tenemos que almacenar un archivo adicional en el servidor.
Ahora le deseo mucha diversión probando el prototipo y espero sus comentarios.