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 las 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 de rendimiento para 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 90. 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 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 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 explica cómo diseñamos el componente de imagen 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 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 significativo 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. Luego, el navegador puede elegir la correcta según el tamaño y la resolución de la pantalla.

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 a menudo no sucede 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 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 usa en la actualidad. Cuando usan srcset, 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 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 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 en 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 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.
  • 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 y implementar 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 conjunto rico de funciones y principios. También incluye opciones que permiten a los desarrolladores personalizarlo para varios requisitos de imagen.

Protección contra 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 Image sin tamaño, el diseño garantiza que se tomen el tiempo 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 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:

  1. 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.
  2. Propiedad imageSizes: Esta también es una propiedad configurable que se usa para obtener los tamaños de imagen correspondientes a los puntos de inflexió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 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%.
Imagen de las montañas tal como se muestra Imagen de las montañas reducida
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.
Imagen de las montañas tal como se muestra La imagen de las montañas que se muestra tal como está no se ajusta a la pantalla.
Diseño = Responsivo: Se reduce o aumenta según el ancho del contenedor en diferentes viewports, y se mantiene la relación de aspecto.
Imagen de montañas aumentada para ajustarse a la pantalla Imagen de montañas reducida para ajustarse a la pantalla
Diseño = Relleno: El ancho y la altura se estiran para llenar el contenedor superior. (el ancho del <div> superior se establece en 300 × 500 en este ejemplo)
Imagen de montañas renderizada para adaptarse al tamaño de 300 × 500 Imagen de montañas renderizada para adaptarse al tamaño de 300 × 500
Imágenes renderizadas para diferentes diseños

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: La compilación de una solución de carga diferida personalizada requiere esfuerzo y un diseño y una 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.

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 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 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 un buen candidato 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 que permitan automatizar la carga previa de imágenes en la mitad superior de la página que cumplan con criterios específicos.

Fomenta el uso de servicios de alojamiento 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 concatenará la URL relativa con la ruta de acceso 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 de mitad inferior de la página o de 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 heredado de JavaScript a Next.js, también actualizó su canalización de imágenes para usar el componente de imagen 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. 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 recomendaban 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 que podían resolver el problema de maneras que no empeoraban el 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 beneficios de rendimiento superiores 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" a sus usuarios, los desarrolladores no querrán 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

Crear una solución que funcione perfectamente en todas las situaciones es muy difícil. 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 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 está diseñado 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á valiosa a medida que admitamos 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.