Cómo crear un componente de imagen efectivo

Un componente de imagen encapsula las prácticas recomendadas de rendimiento y proporciona una solución lista para usar para optimizar imágenes.

Leena Sohoni
Leena Sohoni
Kara Erickson
Kara Erickson
Alex Castle
Alex Castle

Las imágenes son una fuente común de cuellos de botella en el rendimiento de las aplicaciones web y un área clave de enfoque para la optimización. Las imágenes sin optimizar contribuyen al aumento del 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. Existen varias formas de optimizar las imágenes, por lo que se necesita un "componente de imagen" inteligente con soluciones de rendimiento integradas de forma predeterminada.

El equipo de Aurora trabajó con Next.js para crear 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 crear componentes de imágenes en otros frameworks, sistemas de administración de contenido (CMS) y pilas tecnológicas. Colaboramos en un componente similar para Nuxt.js y estamos trabajando con Angular en la optimización de imágenes para versiones futuras. En esta publicación, se explica cómo diseñamos el componente Image de Next.js y las lecciones que aprendimos en el proceso.

Componente de imagen como extensión de imágenes

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 tuvieron 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 vitales como parte de su auditoría de prácticas recomendadas. A continuación, se indican algunas de las áreas comunes en las que las imágenes pueden afectar los Core Web Vitals y la experiencia del usuario.

Las imágenes sin tamaño afectan el CLS

Las imágenes que se publican sin especificar su tamaño pueden causar inestabilidad en el diseño y contribuir a un valor alto de Cumulative Layout Shift (CLS). Configurar los atributos width y height en los elementos <img> puede ayudar a evitar los cambios de diseño. Por ejemplo:

<img src="flower.jpg" width="360" height="240">

El ancho y la altura deben establecerse de manera que la relación de aspecto de la imagen renderizada sea cercana a 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 la relación de aspecto en CSS puede ayudarte a cambiar el tamaño de las imágenes de forma responsiva y, al mismo tiempo, evitar el CLS.

Las imágenes grandes pueden afectar el LCP

Cuanto más grande sea el tamaño de archivo de una imagen, más tiempo tardará en descargarse. Una imagen grande podría ser la imagen "hero" de la página o el elemento más importante del viewport responsable de activar el Largest Contentful Paint (LCP). Una imagen que forma parte del contenido crítico y tarda mucho en descargarse retrasará el LCP.

En muchos casos, los desarrolladores pueden reducir el tamaño de las imágenes con 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. Luego, el navegador puede elegir la adecuada según el tamaño y la resolución de la pantalla.

Una compresión de imagen 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 frecuente. Una mejor compresión reduce el tamaño del archivo entre un 25% y un 50% en algunos casos para la misma calidad de la imagen. Esta reducción permite descargas más rápidas con un menor consumo de datos. La app debe publicar formatos de imagen modernos en los navegadores que los admitan.

La carga de imágenes innecesarias afecta el LCP

Las imágenes que se encuentran en 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 al LCP y lo retrasen. La carga diferida se puede usar para cargar esas imágenes más adelante 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 basadas en 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: Por lo general, los desarrolladores web tienden a enfocarse en el código, JavaScript y la optimización de datos. Por lo tanto, es posible que no conozcan los problemas con las imágenes ni cómo optimizarlas. Es posible que las imágenes creadas por diseñadores o subidas por usuarios no tengan una prioridad alta en la lista.
  • Solución lista para usar: Incluso si los desarrolladores conocen los detalles de la optimización de imágenes, la ausencia de una solución integral lista para usar para su framework o pila tecnológica 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 se obtienen de bases de datos externas o CMS. Puede ser difícil definir el tamaño de esas imágenes cuando la fuente de la imagen es dinámica.
  • Sobrecarga de lenguaje de marcado: Las soluciones para incluir el tamaño de la imagen o srcset para diferentes tamaños requieren lenguaje de marcado adicional para cada imagen, lo que puede ser tedioso. El atributo srcset se introdujo en 2014, pero solo el 26.5% de los sitios web lo usan actualmente. Cuando se usa srcset, los desarrolladores deben crear imágenes en varios tamaños. Las herramientas como just-gimme-an-img pueden ayudar, pero deben usarse manualmente 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 manejo 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 publiquen en 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 en la parte inferior de la página. Elegir la mejor puede ser un desafío. Es posible que los desarrolladores tampoco conozcan la mejor distancia desde el "pliegue" para cargar imágenes diferidas. Los diferentes tamaños de viewport en los dispositivos pueden complicar aún más esta situación.
  • Panorama cambiante: 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 está lanzando la función Fetch Priority 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 estas mejoras se evaluaran y se implementaran a nivel del componente.

El componente de imagen como solución

Las oportunidades disponibles para optimizar imágenes y los desafíos para implementarlas de forma individual en cada aplicación nos llevaron a la idea de un componente de imagen. Un componente de imagen puede encapsular y aplicar prácticas recomendadas. Si reemplazan el elemento <img> por un componente de imagen, los desarrolladores pueden abordar mejor sus problemas de rendimiento de imágenes.

Durante el último año, trabajamos con el framework de Next.js para diseñar y aplicar su componente de imagen. 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 amplio conjunto de funciones y principios. También incluye opciones que permiten a los desarrolladores personalizarlo para diversos requisitos de imágenes.

Protección contra cambios de diseño

Como se mencionó anteriormente, las imágenes sin tamaño definido provocan cambios de diseño y contribuyen al CLS. Cuando usan el componente Image de Next.js, los desarrolladores deben proporcionar un tamaño de la imagen con los atributos width y height para evitar cualquier cambio de diseño. Si se desconoce el tamaño, los desarrolladores deben especificar layout=fill para publicar una imagen sin tamaño 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 durante el tiempo de compilación y, luego, incluirlo 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 Image sin tamaño, el diseño garantiza que se tomarán el tiempo necesario para considerar el tamaño de la imagen y evitar cambios en el diseño.

Facilita la capacidad de respuesta

Para que las imágenes sean responsivas en todos los dispositivos, los desarrolladores deben establecer los atributos srcset y sizes en el elemento <img>. Queríamos reducir este esfuerzo con el componente de imagen. Diseñamos el componente Image 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:

  1. Propiedad deviceSizes: Esta propiedad se puede usar para configurar puntos de interrupción una sola vez según los dispositivos comunes a la base de usuarios de la aplicación. Los valores predeterminados para los puntos de interrupción se incluyen en el archivo de configuración.
  2. 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 de tamaño del dispositivo.
  3. Atributo layout en cada imagen: Se usa para indicar cómo usar las propiedades deviceSizes y imageSizes para cada imagen. Los valores admitidos para el modo de diseño son fixed, fill, intrinsic y responsive.

Cuando se solicita una imagen con los modos de diseño responsivo o relleno, Next.js identifica la imagen que se debe mostrar según el tamaño del dispositivo que solicita la página y establece los atributos 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 que se comparte en la documentación de Next.js y que se visualiza en un teléfono y una laptop estándar.

Pantalla de laptop Pantalla del teléfono
Diseño = Intrínseco: Se reduce para ajustarse al ancho del contenedor en viewports más pequeños. No se amplía más allá del tamaño intrínseco de la imagen en un viewport más grande. El ancho del contenedor está al 100%
La imagen de montañas se muestra tal cual Se redujo la imagen de montañas
Diseño = Fijo: La imagen no es responsiva. El ancho y la altura se fijan de manera similar al elemento "", independientemente del dispositivo en el que se renderiza.
La imagen de montañas se muestra tal cual La imagen de montañas que se muestra tal cual no se ajusta a la pantalla
Diseño = Diseño adaptable: Se reduce o aumenta según el ancho del contenedor en diferentes ventanas gráficas, manteniendo la relación de aspecto.
Imagen de montañas ampliada para ajustarse a la pantalla Imagen de montañas reducida para que quepa en la pantalla
Diseño = Relleno: El ancho y la altura se estiran para rellenar el contenedor principal. (El ancho del elemento <div> principal se establece en 300*500 en este ejemplo).
Imagen de montañas renderizada para que se ajuste al tamaño de 300 x 500 Imagen de montañas renderizada para que se ajuste al tamaño de 300 x 500
Imágenes renderizadas para diferentes diseños

Proporcionar carga diferida integrada

El componente Image proporciona una solución de carga diferida integrada y eficiente de forma predeterminada. Cuando se usa el elemento <img>, hay algunas opciones para la carga diferida, pero todas tienen desventajas que dificultan su uso. Un desarrollador puede adoptar uno de los siguientes enfoques de carga diferida:

  • Especifica el atributo loading: Se admite en todos los navegadores modernos.
  • Usa la API de Intersection Observer: Crear una solución personalizada de carga diferida requiere esfuerzo y un diseño e implementación bien pensados. Es posible que los desarrolladores no siempre tengan tiempo para hacerlo.
  • Importa una biblioteca de terceros para cargar imágenes de forma diferida: Es posible que se requiera un 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.

Precarga imágenes importantes

Con frecuencia, los elementos LCP son imágenes, y las imágenes grandes pueden retrasar el LCP. Es una buena idea precargar las imágenes críticas para que el navegador pueda descubrirlas antes. Cuando se usa un elemento <img>, se puede incluir una sugerencia de carga previa 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 las imágenes, independientemente del framework que se utilice. En el caso del componente Image 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 imágenes también pueden explorar opciones para aplicar heurísticas y automatizar la carga previa de imágenes visibles en la parte 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 imágenes modernos, como WebP y AVIF. El componente Image 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 concatenará la URL relativa con la ruta de CDN para generar la URL absoluta. Se admiten las CDN de imágenes populares, como Imgix, Cloudinary y Akamai. La arquitectura admite el uso de un proveedor de servicios en la nube personalizado a través de 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 Image de Next.js usa un optimizador de imágenes como un servidor de imágenes integrado que proporciona una API similar a la de una 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 quienes buscan crear su propia canalización de optimización de imágenes.

Admite 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 optimiza la experiencia del usuario. Se puede usar en combinación con la carga diferida para las imágenes que se encuentran en la mitad inferior de la página o en la mitad superior.

El componente Image de Next.js admite la carga progresiva de la imagen a través de la propiedad placeholder. 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 Image de Next.js en producción y también estamos trabajando con otras pilas tecnológicas en componentes de imágenes similares.

Cuando Leboncoin migró su frontend heredado de JavaScript a Next.js, también actualizó su canalización de imágenes para usar el componente Image de Next.js. En una página que migró de <img> a next/image, el LCP disminuyó de 2.4 s a 1.7 s. El total de bytes de imágenes descargados para la página pasó de 663 KB a 326 KB (con alrededor de 100 KB de bytes de imágenes cargadas de forma diferida).

Lecciones aprendidas

Cualquier persona que cree una app de Next.js puede beneficiarse del uso del componente Image de Next.js para la optimización. Sin embargo, si deseas 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 beneficio

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. Creímos que sería necesario en los casos en que fuera imposible conocer la altura o el ancho de la imagen con anticipación. Sin embargo, observamos que los usuarios recomiendan el atributo unsized en los problemas de GitHub como una solución integral para los problemas con el requisito de tamaño, incluso en los casos en los que podrían resolver el problema de formas que no empeoraran el CLS. Posteriormente, dimos de baja y quitamos el atributo unsized.

Separa la fricción útil de la molestia sin sentido

El requisito de tamaño de una imagen es un ejemplo de "fricción útil". Restringe el uso del componente, pero, a cambio, proporciona beneficios de rendimiento desproporcionados. Los usuarios aceptarán fácilmente la restricción 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 otros materiales publicados sobre el componente.

Sin embargo, puedes encontrar soluciones alternativas para esa fricción sin sacrificar el rendimiento. Por ejemplo, durante el desarrollo del componente Image de Next.js, recibimos quejas de que era molesto buscar tamaños para las 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 solo impone una "fricción útil" a sus usuarios, los desarrolladores tenderán a no querer usarlo. Descubrimos que, si bien las funciones de rendimiento, como el ajuste de tamaño de las imágenes y la generación automática de valores de srcset, eran las más importantes. Las funciones convenientes para los desarrolladores, como la carga diferida automática y los marcadores de posición borrosos integrados, también generaron interés en el componente Image de Next.js.

Establece una hoja de ruta para las funciones que impulsen la adopción

Es muy difícil crear una solución que funcione a la perfección en todas las situaciones. Puede ser tentador diseñar algo que funcione bien para el 75% de las personas y, luego, decirles al otro 25% que "en estos casos, este componente no es para ustedes".

En la práctica, esta estrategia resulta ser incompatible con tus objetivos como diseñador de componentes. Quieres que los desarrolladores adopten tu componente para aprovechar sus beneficios de rendimiento. Esto es difícil de hacer si hay un grupo de usuarios que no pueden migrar y se sienten excluidos de la conversación. Es probable que expresen decepción, lo que generará percepciones negativas que afectarán la adopción.

Es recomendable que tengas una hoja de ruta para tu componente que abarque todos los casos de uso razonables a largo plazo. También es útil ser explícito en la documentación sobre lo que no se admite y por qué, para establecer expectativas sobre los problemas que el componente pretende resolver.

Conclusión

El uso y la optimización de imágenes son procesos 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 reinvente la rueda cada vez, creamos una plantilla de prácticas recomendadas que los desarrolladores, los frameworks y otras pilas tecnológicas podrían usar como referencia para sus propias implementaciones. Esta experiencia será valiosa a medida que admitamos otros frameworks en sus componentes de imagen.

El componente Image 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 gran modelo que funcionaría bien en el ecosistema más amplio, y nos encantaría recibir comentarios de los desarrolladores que deseen adoptarlo en sus proyectos.