Un componente de imagen encapsula las prácticas recomendadas de rendimiento y proporciona una solución lista para usar con el objetivo de optimizar las imágenes.
Las imágenes son una fuente común de cuellos de botella en el rendimiento de las aplicaciones web y un área de enfoque clave para la optimización. Las imágenes no optimizadas contribuyen al aumento de tamaño de la página y representan más del 70% del peso total de la página en bytes en el percentil 90th. Las múltiples formas de optimizar las imágenes requieren un "componente de imagen" inteligente con soluciones de rendimiento integradas de forma predeterminada.
El equipo de Aurora trabajó con Next.js para compilar uno de esos componentes. El objetivo era crear una plantilla de imagen optimizada que los desarrolladores web pudieran personalizar aún más. El componente sirve como un buen modelo y establece un estándar para compilar componentes de imagen en otros frameworks, sistemas de administración de contenido (CMS) y pilas de tecnología. Colaboramos en un componente similar para Nuxt.js y estamos trabajando con Angular en la optimización de imágenes en versiones futuras. En esta publicación, se analiza cómo diseñamos el componente Next.js Image y las lecciones que aprendimos en el camino.
Problemas y oportunidades de optimización de imágenes
Las imágenes no solo afectan el rendimiento, sino también el negocio. La cantidad de imágenes en una página fue el segundo mejor predictor de conversiones de los usuarios que visitan sitios web. Las sesiones en las que los usuarios generaron conversiones tenían un 38% menos de imágenes que las sesiones en las que no generaron conversiones. Lighthouse enumera varias oportunidades para optimizar las imágenes y mejorar las métricas web como parte de su auditoría de prácticas recomendadas. Estas son algunas de las áreas comunes en las que las imágenes pueden afectar las métricas web esenciales y la experiencia del usuario.
Las imágenes sin tamaño perjudican el CLS
Las imágenes que se entregan sin especificar su tamaño pueden causar inestabilidad en el diseño y contribuir a un cambio de diseño acumulado alto (CLS). Configurar los atributos width
y height
en los elementos img puede ayudar a evitar cambios en el diseño. Por ejemplo:
<img src="flower.jpg" width="360" height="240">
El ancho y la altura deben establecerse de modo que la relación de aspecto de la imagen renderizada esté cerca de su relación de aspecto natural. Una diferencia significativa en la relación de aspecto puede hacer que la imagen se vea distorsionada. Una propiedad relativamente nueva que te permite especificar aspect-ratio en CSS puede ayudar a ajustar el tamaño de las imágenes de forma responsiva y, al mismo tiempo, evitar el CLS.
Las imágenes grandes pueden perjudicar el LCP
Cuanto mayor sea el tamaño del archivo de una imagen, más tardará en descargarse. Una imagen grande puede ser la imagen "hero" de la página o el elemento más importante del viewport, responsable de activar el procesamiento de imagen con contenido más grande (LCP). Una imagen que forma parte del contenido fundamental y tarda mucho en descargarse retrasará el LCP.
En muchos casos, los desarrolladores pueden reducir los tamaños de las imágenes mediante una mejor compresión y el uso de imágenes responsivas. Los atributos srcset
y sizes
del elemento <img>
ayudan a proporcionar archivos de imagen con diferentes tamaños. El navegador puede elegir el correcto en función del tamaño de la pantalla y la resolución.
Una compresión de imágenes deficiente puede afectar el LCP
Los formatos de imagen modernos, como AVIF o WebP, pueden proporcionar una mejor compresión que los formatos JPEG y PNG de uso general. Una mejor compresión reduce el tamaño del archivo de un 25% a un 50% en algunos casos con la misma calidad de la imagen. Esta reducción permite realizar descargas más rápidas con menos consumo de datos. La app debe publicar formatos de imagen modernos en los navegadores que admitan estos formatos.
Cargar imágenes innecesarias perjudica el LCP
Las imágenes que se encuentran debajo de la mitad inferior de la página o que no están en el viewport no se muestran al usuario cuando se carga la página. Se pueden aplazar para que no contribuyan a la LCP y la retrasen. La carga diferida se puede usar para cargar esas imágenes más tarde a medida que el usuario se desplaza hacia ellas.
Desafíos de optimización
Los equipos pueden evaluar el costo de rendimiento debido a los problemas mencionados anteriormente y, luego, implementar soluciones de prácticas recomendadas para superarlos. Sin embargo, esto no suele suceder en la práctica, y las imágenes ineficientes siguen ralentizando la Web. A continuación, se detallan algunos de los motivos posibles:
- Prioridades: Los desarrolladores web suelen enfocarse en el código, JavaScript y la optimización de datos. Por lo tanto, es posible que no estén al tanto de los problemas con las imágenes ni de cómo optimizarlas. Es posible que las imágenes creadas por diseñadores o subidas por los usuarios no sean una prioridad.
- Solución lista para usar: Incluso si los desarrolladores conocen los matices de la optimización de imágenes, la ausencia de una solución todo en uno lista para usar para su framework o pila de tecnología puede ser un factor disuasivo.
- Imágenes dinámicas: Además de las imágenes estáticas que forman parte de la aplicación, los usuarios suben imágenes dinámicas o las obtienen de bases de datos externas o CMS. Puede ser difícil definir el tamaño de esas imágenes en las que la fuente de la imagen es dinámica.
- Sobrecarga de marcas: Las soluciones para incluir el tamaño de imagen o
srcset
para diferentes tamaños requieren lenguaje de marcado adicional para cada imagen, lo que puede ser tedioso. El atributosrcset
se introdujo en 2014, pero solo el 26.5% de los sitios web lo usa actualmente. Cuando usansrcset
, los desarrolladores deben crear imágenes en varios tamaños. Las herramientas como just-gimme-an-img pueden ser útiles, pero se deben usar de forma manual para cada imagen. - Compatibilidad con navegadores: Los formatos de imagen modernos, como AVIF y WebP, crean archivos de imagen más pequeños, pero necesitan un tratamiento especial en los navegadores que no los admiten. Los desarrolladores deben usar estrategias como la negociación de contenido o el elemento
<picture
> para que las imágenes se entreguen a todos los navegadores. - Complicaciones de la carga diferida: Existen varias técnicas y bibliotecas disponibles para implementar la carga diferida de imágenes que se encuentran debajo de la mitad inferior de la página. Elegir la mejor puede ser un desafío. Es posible que los desarrolladores tampoco conozcan la mejor distancia entre el "pliegue" para cargar imágenes diferidas. Los diferentes tamaños de viewport en los dispositivos pueden complicar aún más este problema.
- Cambio de panorama: A medida que los navegadores comienzan a admitir nuevas funciones de HTML o CSS para mejorar el rendimiento, puede ser difícil para los desarrolladores evaluar cada una de ellas. Por ejemplo, Chrome presenta la función Prioridad de recuperación como una prueba de origen. Se puede usar para aumentar la prioridad de imágenes específicas en la página. En general, a los desarrolladores les resultaría más fácil si esas mejoras se evaluaran e implementaran a nivel del componente.
Componente de imagen como solución
Las oportunidades disponibles para optimizar las imágenes y los desafíos de implementarlas de forma individual para cada aplicación nos llevaron a la idea de un componente de imagen. Un componente de imagen puede encapsular y aplicar prácticas recomendadas. Cuando se reemplaza el elemento <img>
por un componente de imagen, los desarrolladores pueden abordar mejor sus problemas de rendimiento de imagen.
Durante el último año, trabajamos con el framework Next.js para diseñar e implementar el componente de imágenes. Se puede usar como reemplazo directo de los elementos <img>
existentes en las apps de Next.js de la siguiente manera.
// Before with <img> element:
function Logo() {
return <img src="/logo.jpg" alt="logo" height="200" width="100" />
}
// After with image component:
import Image from 'next/image'
function Logo() {
return <Image src="/logo.jpg" alt="logo" height="200" width="100" />
}
El componente intenta abordar los problemas relacionados con las imágenes de forma genérica a través de un conjunto rico de funciones y principios. También incluye opciones que permiten a los desarrolladores personalizarlo para varios requisitos de imagen.
Protección contra los cambios de diseño
Como se mencionó anteriormente, las imágenes sin tamaño causan cambios de diseño y contribuyen al CLS. Cuando se usa el componente de imagen de Next.js, los desarrolladores deben proporcionar un tamaño de imagen con los atributos width
y height
para evitar cualquier cambio de diseño. Si el tamaño es desconocido, los desarrolladores deben especificar layout=fill
para entregar una imagen sin tamaño que se encuentra dentro de un contenedor con tamaño. Como alternativa, puedes usar importaciones de imágenes estáticas para recuperar el tamaño de la imagen real en el disco duro en el momento de la compilación y, luego, incluirla en la imagen.
// Image component with width and height specified
<Image src="/logo.jpg" alt="logo" height="200" width="100" />
// Image component with layout specified
<Image src="/hero.jpg" layout="fill" objectFit="cover" alt="hero" />
// Image component with image import
import Image from 'next/image'
import logo from './logo.png'
function Logo() {
return <Image src={logo} alt="logo" />
}
Dado que los desarrolladores no pueden usar el componente de imagen sin tamaño, el diseño se asegura de que se tome el tiempo para considerar el tamaño de la imagen y evitar los cambios de diseño.
Facilita la capacidad de respuesta
Para que las imágenes sean responsivas en todos los dispositivos, los desarrolladores deben configurar los atributos srcset
y sizes
en el elemento <img>
. Queríamos reducir este esfuerzo con el componente Image. Diseñamos el componente de imagen de Next.js para establecer los valores de los atributos solo una vez por aplicación. Los aplicamos a todas las instancias del componente Image según el modo de diseño. Se nos ocurrió una solución de tres partes:
- Propiedad
deviceSizes
: Esta propiedad se puede usar para configurar puntos de interrupción de forma única en función de los dispositivos comunes a la base de usuarios de la aplicación. Los valores predeterminados de los puntos de interrupción se incluyen en el archivo de configuración. - Propiedad
imageSizes
: También es una propiedad configurable que se usa para obtener los tamaños de imagen correspondientes a los puntos de interrupción del tamaño del dispositivo. - El atributo
layout
en cada imagen: se usa para indicar cómo usar las propiedadesdeviceSizes
yimageSizes
en cada imagen. Los valores admitidos para el modo de diseño sonfixed
,fill
,intrinsic
yresponsive
.
Cuando se solicita una imagen con los modos de diseño responsivo o relleno, Next.js identifica la imagen que se publicará en función del tamaño del dispositivo que solicita la página y establece srcset
y sizes
en la imagen de forma adecuada.
En la siguiente comparación, se muestra cómo se puede usar el modo de diseño para controlar el tamaño de la imagen en diferentes pantallas. Usamos una imagen de demostración compartida en la documentación de Next.js, que se ve en un teléfono y una laptop estándar.
Pantalla de laptop | Pantalla del teléfono |
---|---|
Diseño = Intrínseco: Se reduce para adaptarse al ancho del contenedor en viewports más pequeñas. No se ajusta más allá del tamaño intrínseco de la imagen en un viewport más grande. El ancho del contenedor es del 100%. | |
Diseño = Fijo: La imagen no es responsiva. El ancho y la altura son fijos, al igual que el elemento "", independientemente del dispositivo en el que se renderice. | |
Diseño = Responsivo: Puedes reducir o aumentar la escala verticalmente en función del ancho del contenedor en diferentes viewports, manteniendo la relación de aspecto. | |
Diseño = Relleno: El ancho y la altura se estiran para llenar el contenedor superior. (En este ejemplo, el ancho superior de <div> se estableció en 300*500)
|
|
Proporciona carga diferida integrada
El componente Image proporciona una solución de carga diferida integrada y de alto rendimiento de forma predeterminada. Cuando usas el elemento <img>
, hay algunas opciones para la carga diferida, pero todas tienen inconvenientes que las hacen difíciles de usar. Un desarrollador puede adoptar uno de los siguientes enfoques de carga diferida:
- Especifica el atributo
loading
, que es compatible con todos los navegadores modernos. - Usa la API de Intersection Observer: Crear una solución personalizada de carga diferida requiere esfuerzo, y un diseño y una implementación cuidadosos. Es posible que los desarrolladores no siempre tengan tiempo para hacerlo.
- Importar una biblioteca de terceros a las imágenes de carga diferida: Es posible que se requiera esfuerzo adicional para evaluar e integrar una biblioteca de terceros adecuada para la carga diferida.
En el componente Image de Next.js, la carga se establece en "lazy"
de forma predeterminada. La carga diferida se implementa con Intersection Observer, que está disponible en la mayoría de los navegadores modernos. Los desarrolladores no tienen que hacer nada adicional para habilitarla, pero pueden inhabilitarla cuando sea necesario.
Carga previamente imágenes importantes
A menudo, los elementos de LCP son imágenes, y las imágenes grandes pueden retrasar la LCP. Es una buena idea precargar imágenes críticas para que el navegador pueda descubrirlas antes. Cuando usas un elemento <img>
, se puede incluir una sugerencia de precarga en el encabezado HTML de la siguiente manera.
<link rel="preload" as="image" href="important.png">
Un componente de imagen bien diseñado debe ofrecer una forma de ajustar la secuencia de carga de imágenes, independientemente del framework que se use. En el caso del componente de imagen de Next.js, los desarrolladores pueden indicar una imagen que sea una buena candidata para la carga previa con el atributo priority
del componente de imágenes.
<Image src="/hero.jpg" alt="hero" height="400" width="200" priority />
Agregar un atributo priority
simplifica el lenguaje de marcado y es más conveniente de usar. Los desarrolladores de componentes de imagen también pueden explorar opciones para aplicar heurísticas y automatizar la precarga de imágenes en la mitad superior de la página que cumplan con criterios específicos.
Fomenta el hosting de imágenes de alto rendimiento
Se recomiendan las CDN de imágenes para automatizar la optimización de imágenes y también admiten formatos de imagen modernos, como WebP y AVIF. El componente de imagen de Next.js usa una CDN de imágenes de forma predeterminada con una arquitectura de cargador. En el siguiente ejemplo, se muestra que el cargador permite configurar la CDN en el archivo de configuración de Next.js.
module.exports = {
images: {
loader: 'imgix',
path: 'https://ImgApp/imgix.net',
},
}
Con esta configuración, los desarrolladores pueden usar URLs relativas en la fuente de la imagen, y el framework concatena la URL relativa con la ruta de CDN para generar la URL absoluta. Se admiten CDN de imágenes populares, como Imgix, Cloudinary y Akamai. La arquitectura admite el uso de un proveedor de servicios en la nube personalizado mediante la implementación de una función loader
personalizada para la app.
Compatibilidad con imágenes alojadas en una ubicación propia
Puede haber situaciones en las que los sitios web no puedan usar CDN de imágenes. En esos casos, un componente de imagen debe admitir imágenes alojadas por el usuario. El componente de imagen de Next.js usa un optimizador de imágenes como un servidor de imágenes integrado que proporciona una API similar a la de CDN. El optimizador usa Sharp para las transformaciones de imágenes de producción si está instalado en el servidor. Esta biblioteca es una buena opción para cualquiera que quiera compilar su propia canalización de optimización de imágenes.
Cómo admitir la carga progresiva
La carga progresiva es una técnica que se usa para mantener el interés de los usuarios mostrando una imagen de marcador de posición, por lo general, de calidad significativamente inferior mientras se carga la imagen real. Mejora el rendimiento percibido y la experiencia del usuario. Se puede usar en combinación con la carga diferida para imágenes en la mitad inferior de la página o en la mitad superior de la página.
El componente de imagen de Next.js admite la carga progresiva de la imagen a través de la propiedad marcador de posición. Se puede usar como LQIP (marcador de posición de imagen de baja calidad) para mostrar una imagen borrosa o de baja calidad mientras se carga la imagen real.
Impacto
Con todas estas optimizaciones incorporadas, hemos tenido éxito con el componente de imagen de Next.js en producción y también estamos trabajando con otras pilas de tecnología en componentes de imagen similares.
Cuando Leboncoin migró su frontend de JavaScript heredado a Next.js, también actualizó la canalización de imágenes para usar el componente de imagen de Next.js. En una página que se migró de <img>
a la siguiente/imagen, el LCP disminuyó de 2.4 s a 1.7 s. La cantidad total de bytes de imagen descargados para la página pasó de 663 KB a 326 KB (con alrededor de 100 KB de bytes de imagen cargados de forma diferida).
Lecciones aprendidas
Cualquier persona que cree una app de Next.js puede beneficiarse de usar el componente de imagen de Next.js para la optimización. Sin embargo, si quieres crear abstracciones de rendimiento similares para otro framework o CMS, a continuación, se incluyen algunas lecciones que aprendimos en el camino y que podrían ser útiles.
Las válvulas de seguridad pueden causar más daño que bien
En una versión preliminar del componente Image de Next.js, proporcionamos un atributo unsized
que permitía a los desarrolladores omitir el requisito de tamaño y usar imágenes con dimensiones no especificadas. Pensamos que esto sería necesario en los casos en que fuera imposible conocer la altura o el ancho de la imagen con anticipación. Sin embargo, notamos que los usuarios recomendaron el atributo unsized
en GitHub como una solución general para los problemas con el requisito de tamaño, incluso en casos en los que podrían resolver el problema de formas que no empeoraron la CLS. Posteriormente, dejamos de admitir el atributo unsized
y lo quitamos.
Separa la fricción útil de la molestia sin sentido
El requisito de ajustar el tamaño de una imagen es un ejemplo de "fricción útil". Restringe el uso del componente, pero proporciona grandes beneficios de rendimiento a cambio. Los usuarios aceptarán la restricción con gusto si tienen una idea clara de los posibles beneficios de rendimiento. Por lo tanto, vale la pena explicar esta compensación en la documentación y en otros materiales publicados sobre el componente.
Sin embargo, puedes encontrar soluciones para esa fricción sin sacrificar el rendimiento. Por ejemplo, durante el desarrollo del componente de imagen de Next.js, recibimos quejas de que era molesto buscar tamaños para imágenes almacenadas de forma local. Agregamos importaciones de imágenes estáticas, que optimizan este proceso recuperando automáticamente las dimensiones de las imágenes locales en el tiempo de compilación con un complemento de Babel.
Logra un equilibrio entre las funciones de conveniencia y las optimizaciones de rendimiento
Si tu componente de imagen no hace más que imponer una "fricción útil" para sus usuarios, los desarrolladores tienden a no querer usarlo. Descubrimos que, aunque las funciones de rendimiento, como el tamaño de las imágenes y la generación automática de valores de srcset
, fueron las más importantes, Las funciones de conveniencia para desarrolladores, como la carga diferida automática y los marcadores de posición borrosos integrados, también generaron interés en el componente de imagen de Next.js.
Establece un plan de ruta para las funciones para impulsar la adopción
Es muy difícil compilar una solución que funcione a la perfección para todas las situaciones. Puede ser tentador diseñar algo que funcione bien para el 75% de las personas y, luego, decirle al otro 25% que “en estos casos, este componente no es para ti”.
En la práctica, esta estrategia resulta incompatible con tus objetivos como diseñador de componentes. Quieres que los desarrolladores adopten tu componente para beneficiarse de sus ventajas de rendimiento. Esto es difícil de hacer si hay un contingente de usuarios que no pueden migrar y se sienten excluidos de la conversación. Es probable que expresen su decepción, lo que generará percepciones negativas que afectarán la adopción.
Te recomendamos que tengas una hoja de ruta para tu componente que abarque todos los casos de uso razonables a largo plazo. También ayuda ser explícito en la documentación sobre lo que no se admite y por qué, a fin de establecer expectativas sobre los problemas que el componente está pensado para resolver.
Conclusión
El uso y la optimización de imágenes son complicados. Los desarrolladores deben encontrar el equilibrio entre el rendimiento y la calidad de las imágenes, a la vez que garantizan una excelente experiencia del usuario. Esto hace que la optimización de imágenes sea una tarea de alto costo y alto impacto.
En lugar de que cada app reinventara la rueda cada vez, creamos una plantilla de prácticas recomendadas que los desarrolladores, los frameworks y otras pilas de tecnología podrían usar como referencia para sus propias implementaciones. Esta experiencia será realmente valiosa, ya que admitimos otros frameworks en sus componentes de imagen.
El componente de imagen de Next.js mejoró con éxito los resultados de rendimiento en las aplicaciones de Next.js, lo que mejoró la experiencia del usuario. Creemos que es un modelo excelente que funcionaría bien en el ecosistema más amplio y nos encantaría saber de los desarrolladores que quieran adoptar este modelo en sus proyectos.