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 a fin de 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 no optimizadas aumentan el sobredimensionamiento de la página y actualmente representan más del 70% del peso total de la página en bytes en el percentil 90. Hay varias formas de optimizar las imágenes, que requieren un “componente de imagen” inteligente con soluciones de rendimiento integradas de forma predeterminada.

El equipo de Aurora trabajó con Next.js para crear un componente de ese tipo. El objetivo era crear una plantilla de imagen optimizada que los desarrolladores web pudieran personalizar aún más. El componente funciona 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 tecnológicas. Trabajamos en un componente para Nuxt.js similar 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 durante el proceso.

Componente de imagen como extensión de las imágenes

Oportunidades y problemas de optimización de las 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 mayor predictor de las 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 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 afectan la CLS

Las imágenes publicadas sin su tamaño especificado pueden causar inestabilidad en el diseño y contribuir a un gran Cambio de diseño acumulado (CLS). Configurar los atributos width y height en los elementos img puede ayudar a evitar cambios de diseño. Por ejemplo:

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

El ancho y la altura se deben configurar de modo que la relación de aspecto de la imagen renderizada se acerque 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 ayudar a ajustar el tamaño de las imágenes de forma responsiva y evitar la CLS.

Las imágenes grandes pueden dañar el LCP

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

En muchos casos, los desarrolladores pueden reducir el tamaño 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 adecuado según el tamaño y la resolución de la pantalla.

Una compresión de imágenes deficiente puede dañar 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 entre un 25% y un 50% en algunos casos con la misma calidad de la imagen. Esta reducción da lugar a descargas más rápidas con menos consumo de datos. La app debería publicar formatos de imagen modernos en navegadores que admitan estos formatos.

Cargar imágenes innecesarias afecta el LCP

Las imágenes en la mitad inferior de la página o no en el viewport no se muestran al usuario cuando se carga la página. Se pueden postergar para que no contribuyan al LCP y lo 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 enumerados anteriormente e implementar soluciones con las prácticas recomendadas para resolverlos. Sin embargo, esto no suele suceder en la práctica, y las imágenes ineficientes siguen ralentizando la Web. A continuación, se detallan algunas de las razones posibles:

  • Prioridades: Los desarrolladores web suelen enfocarse en el código, JavaScript y en 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 las primeras en la lista de prioridades.
  • 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 lista para usar en 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 las imágenes dinámicas o que provienen de bases de datos externas o de CMS. Definir el tamaño de las imágenes en las que la fuente es dinámica puede ser un desafío.
  • Sobrecarga de marca: Las soluciones para incluir el tamaño de la imagen o srcset para diferentes tamaños requieren un 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. Cuando usan srcset, los desarrolladores deben crear imágenes en varios tamaños. Algunas herramientas, como just-gimme-an-img, pueden ser útiles, pero se deben utilizar 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 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 carga diferida: Hay varias técnicas y bibliotecas disponibles para implementar la carga diferida en las imágenes de la mitad inferior de la página. Elegir la mejor puede ser un desafío. También es posible que los desarrolladores no 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 este proceso.
  • Cambio en el panorama: A medida que los navegadores comienzan a admitir nuevas funciones de HTML o CSS para mejorar el rendimiento, es posible que sea 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 de los componentes.

Componente de imagen como solución

Las oportunidades disponibles para optimizar imágenes y los desafíos de implementarlas individualmente 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. Mediante el reemplazo del elemento <img> con un componente de imagen, los desarrolladores pueden abordar mejor los problemas de rendimiento de las imágenes.

Durante el último año, trabajamos con el framework Next.js para diseñar e implement su componente de imagen. Se puede usar como reemplazo directo de los elementos <img> existentes en las apps 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 problemas relacionados con la imagen de manera genérica a través de un amplio conjunto de características 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 explicó anteriormente, las imágenes sin tamaño causan cambios en el diseño y contribuyen al CLS. Cuando se usa el componente de imagen Next.js, los desarrolladores deben proporcionar un tamaño de imagen con los atributos width y height para evitar cambios en el diseño. Si se desconoce el tamaño, los desarrolladores deben especificar layout=fill para entregar una imagen sin tamaño que se encuentre dentro de un contenedor del tamaño. De manera 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 e 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 necesitarán el tiempo para considerar el tamaño de la imagen y evitar cambios de diseño.

Facilitar 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 Image. Diseñamos el componente de imagen Next.js para establecer los valores del atributo 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 por única vez en función de los dispositivos comunes de 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 del tamaño del dispositivo.
  3. El 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 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 manera 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 compartió en los documentos de Next.js en un teléfono y una laptop estándar.

Pantalla de laptop Pantalla del teléfono
Diseño = Intrínseco: reduce la escala verticalmente para adaptarse al ancho del contenedor en viewports más pequeños. No se escala verticalmente más allá del tamaño intrínseco de la imagen en un viewport más grande. El ancho del contenedor está al 100%.
Imagen de las montañas que se muestra Imagen de las montañas reducida
Diseño = Corregido: la imagen no responde. El ancho y la altura se fijan de manera similar al elemento "", independientemente del dispositivo en el que se renderiza.
Imagen de las montañas que se muestra La imagen de las montañas que se muestra no entra en la pantalla.
Diseño = responsivo: Aumenta o reduce la escala verticalmente según el ancho del contenedor en diferentes viewports, manteniendo la relación de aspecto.
Imagen de montañas ajustada a la pantalla Imagen de montañas reducida para adaptarla a la pantalla
Diseño = Relleno: El ancho y la altura se estiraron para llenar el contenedor superior. (superior `
En este ejemplo, el ancho se establece en 300*500).
Imagen de montañas renderizadas para adaptarse a un tamaño de 300*500 Imagen de montañas renderizadas para adaptarse a un tamaño de 300*500
Imágenes renderizadas para diferentes diseños

Proporciona carga diferida integrada

El componente de imagen proporciona una solución de carga diferida integrada y eficaz de forma predeterminada. Cuando se usa el elemento <img>, hay algunas opciones nativas para la carga diferida, pero todas tienen desventajas que las hacen difíciles de usar. Un desarrollador puede adoptar uno de los siguientes enfoques de carga diferida:

  • Especifica el atributo loading. Es fácil de implementar, pero, por el momento, no es compatible con algunos navegadores.
  • Usa la API de Intersection Observer: La compilación de una solución de carga diferida personalizada requiere esfuerzo y una implementación y un diseño cuidadosos. Es posible que los desarrolladores no siempre tengan tiempo para hacer esto.
  • Importar 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 de imagen Next.js, la carga se configura en "lazy" de forma predeterminada. La carga diferida se implementa con Intersection Observer, que está disponible en la mayoría de los navegadores modernos. No es necesario que los desarrolladores realicen acciones adicionales para habilitarla, pero pueden inhabilitarla cuando sea necesario.

Precarga imágenes importantes

Muy a menudo, los elementos LCP son imágenes, y las imágenes grandes pueden retrasar el LCP. Se recomienda precargar imágenes importantes para que el navegador pueda descubrirlas antes. Cuando se usa 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 las imágenes, independientemente del framework utilizado. En el caso del componente de imagen Next.js, los desarrolladores pueden indicar una imagen que sea una buena candidata para la precarga 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 para automatizar la precarga de imágenes en la mitad superior de la página que cumplan con criterios específicos.

Incentiva 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 Next.js usa una CDN de imágenes de forma predeterminada mediante una arquitectura de cargador. En el siguiente ejemplo, se muestra que el cargador permite la configuración de la CDN en el archivo de configuración 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 la 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 autoalojadas

Puede haber situaciones en las que los sitios web no puedan utilizar CDN de imágenes. En esos casos, un componente de imagen debe admitir imágenes autoalojadas. El componente de imagen Next.js utiliza un optimizador de imágenes como un servidor de imágenes integrado que proporciona una API similar a 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 busque 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 una calidad significativamente más baja mientras se carga la imagen real. Mejora el rendimiento percibido y mejora la experiencia del usuario. Se puede usar en combinación con la carga diferida para imágenes de la mitad inferior de la página o en la mitad superior de la página.

El componente de imagen Next.js admite la carga progresiva de la imagen a través de la propiedad placeholder. Se puede usar como un LQIP (marcador de posición de imagen de baja calidad) para mostrar una imagen de baja calidad o desenfocada mientras se carga la imagen real.

Impacto

Después de incorporar todas las optimizaciones anteriores, tuvimos éxito con el componente de imagen Next.js en la producción. Además, estamos trabajando con otras pilas tecnológicas en componentes de imagen similares.

Cuando Leboncoin migró su frontend de JavaScript heredado a Next.js, también actualizó su canalización de imágenes para usar el componente Next.js Image. En una página que se migró de <img> a siguiente/imagen, el LCP bajó 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 unos 100 kB de bytes de imagen de carga diferida).

Lecciones aprendidas

Cualquier persona que cree una app de Next.js puede beneficiarse del uso del componente de imagen Next.js para su optimización. Sin embargo, si deseas compilar abstracciones de rendimiento similares para otro framework o CMS, a continuación, se incluyen algunas lecciones que aprendimos durante el proceso y que podrían ser útiles.

Las válvulas de seguridad pueden causar más daño de lo bueno

En una versión preliminar del componente de imagen 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 saber de antemano la altura o el ancho de la imagen. Sin embargo, notamos que los usuarios recomiendan el atributo unsized en los problemas de GitHub como una solución genérica a los problemas con el requisito de tamaño, incluso en casos en los que pudieron resolver el problema de maneras que no empeoraron CLS. Posteriormente, dio de baja el atributo unsized y lo quitamos.

Separa la fricción útil de la molestia inútil

El requisito de tamaño de una imagen es un ejemplo de "fricción útil". Restringe el uso del componente, pero a cambio proporciona grandes beneficios de rendimiento. Los usuarios aceptarán la restricción con facilidad si tienen un panorama claro de los posibles beneficios de rendimiento. Por lo tanto, vale la pena explicar esta compensación en la documentación y en otro material publicado acerca del componente.

Sin embargo, puedes encontrar soluciones para esa fricción sin sacrificar el rendimiento. Por ejemplo, durante el desarrollo del componente de imagen Next.js, recibimos reclamos de que era molesto buscar los tamaños de las imágenes almacenadas de forma local. Agregamos las importaciones de imágenes estáticas, que optimizan este proceso mediante la recuperación automática de las dimensiones de las imágenes locales durante el tiempo de compilación con un complemento de Babel.

Busca el equilibrio entre las funciones prácticas y las optimizaciones de rendimiento

Si el componente de imagen solo impone una "fricción útil" a los usuarios, los desarrolladores no querrán usarlo. Descubrimos que, aunque las funciones de rendimiento, como el tamaño de la imagen y la generación automática de valores de srcset, eran las más importantes. Las funciones prácticas para desarrolladores, como la carga diferida automática y los marcadores de posición desenfocados integrados, también generaron interés en el componente de imagen Next.js.

Define una hoja de ruta para las funciones que impulsen 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 25% restante que "en estos casos, este componente no es para ti".

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

Te recomendamos tener una hoja de ruta para tu componente que abarque todos los casos de uso razonables a largo plazo. También ayuda a 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 complicados. Los desarrolladores deben encontrar el equilibrio entre el rendimiento y la calidad de las imágenes y, al mismo tiempo, garantizar 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, se nos ocurrió 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, en efecto, será valiosa a medida que brindemos asistencia para otros frameworks en sus componentes de imagen.

El componente de imagen Next.js mejoró con éxito los resultados de rendimiento de 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 escuchar a los desarrolladores que quieran adoptar este modelo en sus proyectos.