Ajustes automáticos en las imágenes optimizadas por Angular
Cómo la directiva NgOptimizedImage calibra las imágenes para ser representadas eficientemente
En el ámbito del desarrollo web, optimizar las imágenes para una adecuada representación visual es crucial. Este artículo resalta los ajustes que efectúa la directiva NgOptimizedImage
, esa característica del framework de Angular que permite de manera sencilla comunicar a los navegadores cuáles imágenes priorizar y cuáles posponer, además de ayudar con la configuración de imágenes adaptativas (en inglés, responsive) y configurar automáticamente marcadores de posición de imágenes, mejorando la velocidad de carga percibida del contenido web para nuestros usuarios.
Comencemos activando la directiva NgOptimizedImage
importándola desde el paquete @angular/common
y reemplazando el atributo src
del elemento de imagen, en el ejemplo de código, con el atributo ngSrc
. Esta sustitución transfiere el control del elemento a la directiva, que genera durante la compilación el <img>
resultante con enlaces a imágenes cargadas selectivamente por el navegador de acuerdo con las capacidades del dispositivo y las condiciones de la red del usuario.
En el ejemplo, obtenemos la imagen desde Unsplash, la biblioteca de fotos adoptada oficialmente por la plataforma de publicación Medium, utilizando un cargador de imágenes personalizado.
Optar por un cargador de imágenes personalizado o uno incorporado — de los que ofrecen soporte para servicios de imágenes de terceros como Cloudflare, Imgix y Netlify — en vez de usar un cargador genérico sin transformaciones de URL, permite una mayor optimización a través de características específicas del CDN, como el redimensionamiento, selección del formato y la calidad de la imagen.
Acá está el HTML resultante de la plantilla del componente Angular después de la compilación en modo de producción:
<app-optimized-unsplash-image>
<article>
<img
alt="Optimized Unsplash image"
ngsrc="0*hiSjMcbdqbV35wRI"
width="600"
height="400"
decoding="sync"
priority=""
loading="eager"
fetchpriority="high"
ng-img="true"
src="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
srcset="https://cdn-images-1.medium.com/max/600/0*hiSjMcbdqbV35wRI 1x,
https://cdn-images-1.medium.com/max/1200/0*hiSjMcbdqbV35wRI 2x">
</article>
</app-optimized-unsplash-image>
Optimizando la carga de imágenes
La directiva NgOptimizedImage
utiliza el atributo priority
para marcar las imágenes a las que el navegador debe otorgar prioridad de procesamiento, mejorando su tiempo de carga ante indicadores de rendimiento web como la Web Vital LCP.
Esta métrica mide la rapidez con la que el navegador puede representa el elemento visible más grande que tiene contenido de texto, imagen o video. Este uno de los pasos culminantes de la ruta de representación crítica (en inglés, Critical Rendering Path) que nos da la confianza de que nuestros usuarios recibirán contenido relevante en tiempo.
Esta secuencia de carga inicia cuando el navegador comienza a buscar y analizar el documento HTML para convertir el marcado y las reglas de estilo en estructuras lógicas, como los árboles DOM y CSSOM. Al mismo tiempo, el código JavaScript analizado está siendo compilado e interpretado, manipulando estos árboles antes de que se ejecuten los cálculos geométricos y de composición visual de la manera más eficiente posible para poder dibujar los elementos web resultantes en la pantalla.
El panel de Rendimiento de la herramienta DevTools puede ayudarnos a visualizar e inspeccionar cualquier cuello de botella para optimizar la secuencia de representación y alcanzar el evento LCP lo antes posible.
En modo de desarrollo, la directiva NgOptimizedImage
ejecuta un mecanismo de detección basado en un PerformanceObserver
que vigila los eventos LCP. Supongamos que el elemento LCP resulta ser una imagen optimizada por Angular que no tiene el atributo priority
. En este caso, la consola del navegador nos va a mostrar un error en tiempo de ejecución alertándonos a etiquetar la imagen adecuadamente para obtener mejores resultados de rendimiento web.
La directiva entonces puede ajustar automáticamente el comportamiento de carga de las imágenes basándose en la presencia del atributo priority
. Las imágenes con esta instrucción reciben los atributos fetchpriority="high"
y loading="eager"
, indicando al navegador que las obtenga y represente visualmente tan pronto como su nodo HTML sea procesado.
Las imágenes restantes, consideradas no esenciales, reciben el atributo loading="lazy"
, permitiendo al navegador centrarse primero en representar el contenido más prioritario. Esta estrategia (introducida como característica nativa en los navegadores basados en Chromium en 2019 y totalmente disponible en todos los navegadores modernos desde 2022) pospone la carga de estas imágenes hasta que el navegador estime que sean necesarias — por ejemplo, cuando estén casi a la vista a medida que un usuario se desplaza o hace clic hacia ellas.
Noten que el valor del atributo decoding="sync"
fue adicionado manualmente a la imagen prioritaria como una pista adicional al navegador para que la decodifique junto con otras tareas de procesamiento. Y a la viceversa, para obtener una mejora adicional del rendimiento en imágenes cargadas "perezosamente", podemos declarar el atributo decoding="async"
para permitir que otro contenido se represente visualmente antes de que los datos de imagen desde un formato comprimido sean decodificados previo a la etapa que finalmente dibuja los píxeles en pantalla.
También pueden notar que todas las imágenes procesadas por la directiva NgOptimizedImage
están marcadas con el atributo ng-img
, para que podamos identificarlas con precisión durante las pruebas sintéticas o el monitoreo de usuarios reales y medir su verdadero impacto en el rendimiento general de la aplicación.
Construyendo una imagen adaptativa
Otro atributo que la directiva NgOptimizedImage
genera automáticamente es un srcset
de imágenes cuando un cargador personalizado o de terceros es utilizado, listando imágenes candidatas para que los navegadores visualicen bajo condiciones adaptativas específicas, como la dimensión del puerto de visualización (del inglés viewport) o la densidad de píxeles de la pantalla del dispositivo. Esta característica nativa del navegador no solo potencialmente reduce el tamaño de descarga de las imágenes, sino que también apoya la creación de diseños adaptativos, optimizando las experiencias de usuario a través de dispositivos diferentes.
Si el atributo sizes
no está presente como en el ejemplo anterior, se calculará un srcset
fijo utilizando los descriptores de densidad de píxeles predeterminados 1x y 2x, más la información obtenida del atributo width
y el parámetro config.width
utilizado en el cargador de imágenes.
Para una mejor dirección artística, es óptimo especificar el atributo sizes
con valores que reflejen el ancho de imagen esperado para cada condición visual. La directiva NgOptimizedImage
establece por defecto en dieciséis puntos de interrupción (en inglés, breakpoints), por lo que proporcionar nuestra propia lista basada en el diseño de nuestra aplicación puede reducir la longitud de los valores de srcset
generados automáticamente a nuestros requerimientos específicos.
El resultado HTML compilado ahora muestra un srcset
personalizado generado por la directiva utilizando la información del atributo sizes
.
<app-optimized-unsplash-image>
<article>
<img
alt="Optimized Unsplash Image"
ngsrc="0*hiSjMcbdqbV35wRI"
width="600"
height="400"
decoding="sync"
sizes="(max-width: 400px) 100vw,
(max-width: 1200px) 50vw,
100vw"
priority=""
loading="eager"
fetchpriority="high"
ng-img="true"
src="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
srcset="https://cdn-images-1.medium.com/max/380/0*hiSjMcbdqbV35wRI 380w,
https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI 640w,
https://cdn-images-1.medium.com/max/1200/0*hiSjMcbdqbV35wRI 1200w,
https://cdn-images-1.medium.com/max/1920/0*hiSjMcbdqbV35wRI 1920w,
https://cdn-images-1.medium.com/max/2048/0*hiSjMcbdqbV35wRI 2048w,
https://cdn-images-1.medium.com/max/3840/0*hiSjMcbdqbV35wRI 3840w">
</article>
</app-optimized-unsplash-image>
Si necesitásemos proporcionar imágenes candidatas utilizando descriptores de densidad de píxeles, la directiva NgOptimizedImage
nos permite introducir en una entrada ngSrcset una lista de descriptores de ancho o densidad de píxeles, pero no una combinación de ambos. Intentar declarar valores combinados como ngSrcset="400w, 600w, 1200w, 1x, 2x"
genera el siguiente error en tiempo de ejecución.
Además, definir valores de descriptores de densidad de píxeles demasiado altos devuelve un error en modo de desarrollo. La directiva está diseñada para soportar hasta un máximo de 3x de densidad, con la recomendación de no exceder 2x basada en las capacidades del sistema visual humano. Valores más altos aumentarían innecesariamente del tamaño del archivo sin una mejora perceptible en la claridad de la imagen para el usuario, lo que conduce a tiempos de carga más lentos.
Si necesitamos controlar de una forma más detallada la selección de imágenes a partir de condiciones adaptativas complejas, es una opción válida optar porque el atributo srcset
no se genere automáticamente al usar cargadores de imágenes. Esto se puede lograr fácilmente estableciendo el atributo disableOptimizedSrcset="true"
en la etiqueta de la imagen optimizada.
Generando sugerencias de carga especulativa
Como desarrolladores, tenemos la oportunidad de informar a los navegadores sobre qué imágenes son candidatas para solicitudes de procesamiento anticipado antes de que se cumplan sus condiciones de representación visual.
El constructor de paquetes para navegadores (en inglés, browser builder) de Angular realiza una búsqueda rápida para encontrar dominios listados en el cargador de imágenes. Al detectar una coincidencia que aún no ha sido declarada manualmente, inserta un enlace de preconexión en el encabezado del documento, con el atributo data-ngimg
indicando que el elemento <link>
fue añadido automáticamente durante el tiempo de construcción.
<head>
<!-- Elementos adicionales de la sección <head> -->
<!-- Enlace de preconexión al CDN de Medium, añadido automáticamente -->
<link
rel="preconnect"
href="https://cdn-images-1.medium.com"
data-ngimg>
</head>
Esta forma de carga especulativa indica al navegador web que debe priorizar la búsqueda DNS, la negociación TLS y el establecimiento de la conexión TCP con el servidor de imágenes que va a ser probablemente el encargado de servir el elemento LCP.
Adicionalmente, en modo de desarrollo, hay un verificador de enlaces de preconexión en tiempo de ejecución que verifica si existe la sugerencia correspondiente para optimizar imágenes marcadas con el atributo priority
. Cuando el verificador no puede confirmar esto, la consola del navegador muestra una advertencia.
De ser necesario, podemos informar al verificador de enlaces sobre dominios dedicados al desarrollo y pruebas de software. Dado que estos generalmente no requieren sugerencias de preconexión, especificarlos en el token PRECONNECT_CHECK_BLOCKLIST
es una buena idea para deshabilitar advertencias innecesarias. Por defecto, las direcciones localhost
, 127.0.0.1
, y 0.0.0.0
están incluidas en esta lista de bloqueo.
{
provide: PRECONNECT_CHECK_BLOCKLIST,
useValue: [
'https://dev-domain.com',
'https://test-domain.com',
'https://another-excluded-domain.com'
]
}
Cuando la aplicación de Angular es entregada empleando SSR (Representación del lado del servidor), la directiva añade un elemento de enlace de precarga al encabezado del documento. Esta sugerencia va más allá del preconnect
al solicitar al navegador que inicie tanto la conexión como la descarga anticipada de la imagen, mejorando la percepción de velocidad de carga del usuario a través de la obtención prioritaria de elementos candidatos a LCP.
<head>
<!-- Elementos adicionales de la sección <head> -->
<!-- Enlace de precarga a la imagen prioritaria, añadido automáticamente en modo SSR -->
<link
as="image"
href="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
rel="preload"
fetchpriority="high">
</head>
Las imágenes adaptativas tienen los atributos imagesizes
e imagesrcset
añadidos al elemento <link>
cuando los correspondientes atributos sizes
o srcset
son declarados en la imagen optimizada.
<head>
<!-- Elementos adicionales de la sección <head> -->
<!-- Enlace de precarga a la imagen prioritaria adaptativa, añadido automáticamente en modo SSR -->
<link
as="image"
href="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
rel="preload"
fetchpriority="high"
imagesizes="(max-width: 400px) 100vw,
(max-width: 1200px) 50vw,
100vw"
imagesrcset="https://cdn-images-1.medium.com/max/380/0*hiSjMcbdqbV35wRI 380w,
https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI 640w,
https://cdn-images-1.medium.com/max/1200/0*hiSjMcbdqbV35wRI 1200w,
https://cdn-images-1.medium.com/max/1920/0*hiSjMcbdqbV35wRI 1920w,
https://cdn-images-1.medium.com/max/2048/0*hiSjMcbdqbV35wRI 2048w,
https://cdn-images-1.medium.com/max/3840/0*hiSjMcbdqbV35wRI 3840w">
</head>
También podemos dar varios pasos hacia adelante incluyendo manualmente nuestras propias reglas especulativas en un elemento <script type="speculationrules">
, siguiendo la Speculation Rules API (que aún se encuentra en modo experimental). Éstas nos brindan un control granular sobre las estrategias de precarga y pre-representación, aprovechando el tiempo de inactividad del navegador y los recursos de red. También evitan la precarga en modos de ahorro de batería y de ahorro de datos, y ayudan a proteger la privacidad de los datos cuando la precarga ocurre entre diferentes dominios.
Incluyendo vista previa de imágenes
Angular en la versión 17.2 introdujo las vistas previas o marcadores para imágenes (en inglés, placeholders), con una mínima configuración básica que requiere solamente añadir el atributo placeholder
al elemento de imagen optimizada. Cuando se especifica, la directiva solicita una miniatura de la imagen, presentándola como una vista previa difuminada para crear un efecto de transición hasta que la imagen de alta resolución esté completamente cargada.
El ancho predeterminado de la miniatura es de 30px, ajustable de ser necesario a través de la propiedad placeholderResolution
en el proveedor IMAGE_CONFIG
de la directiva.
La directiva NgOptimizedImage
define algunas reglas CSS dentro del atributo style
de su elemento anfitrión para alargar condicionalmente la miniatura de baja resolución y cubrir, como imagen de fondo, toda el área del contenedor. La propiedad CSS filter: blur(...)
es aplicada de manera opcional para lograr que la imagen pixelada se difumine.
Tengamos en cuenta que cuando el atributo placeholder
está definido, pero no se proporciona un cargador de imágenes, la directiva corre el riesgo de declarar la imagen de mayor resolución como su propia vista previa, lo que lleva a transformaciones de estilo innecesarias.
En modo de desarrollo, ocurre un error en tiempo de ejecución si esta condición se cumple, aconsejándonos solucionar este problema.
De todos modos, la directiva ofrece una manera de mostrar un marcador de posición personalizado al usar un cargador genérico, permitiendo pasar una miniatura codificada en base64 en el atributo placeholder
. Menos de 4000 caracteres debería ser suficiente para codificar una imagen de baja resolución de 20 x 20 píxeles. Cuando excedemos esta longitud se genera una advertencia en la consola del navegador en modo de desarrollo. Si la longitud de la imagen codificada supera los 10000 caracteres, se lanza un error en tiempo de ejecución para evitar un aumento no deseado en el tamaño de la plantilla HTML que puede socavar nuestros esfuerzos de mejora del rendimiento.
Vale la pena mencionar que el concepto de marcador de posición a nivel de imagen optimizada no está conectado con los de las vistas diferibles, ya que los sub-bloques @placeholder
son visualizados hasta que sus condiciones diferidas son satisfechas. Sin embargo, podemos hacer que se complementen entre sí, como sugiere el siguiente ejemplo:
Conclusión
Como hemos explorado, el uso adecuado de la directiva NgOptimizedImage
no solo le permite llevar a cabo automáticamente la automatización interna que ayuda al navegador a tomar decisiones sobre la representación de imágenes, sino que también mejora el rendimiento general de la aplicación y la experiencia de usuario.
Les invito a que consulten el siguiente repositorio de GitHub con el código referenciado en este artículo, para continuar experimentando por su cuenta cómo es que Angular genera el HTML para elementos de imagen optimizadas: