Capítulo 26 - Parte 2: NgOptimizedImage: imágenes optimizadas out-of-the-box¶
Parte 2 de 4 · Capítulo 26 · PARTE XII - Optimización y Rendimiento
Las imágenes son históricamente el mayor culpable de un LCP (Largest Contentful Paint) deficiente en aplicaciones web. Angular 15 introdujo NgOptimizedImage, una directiva que aplica automáticamente las mejores prácticas de carga de imágenes: preload de imágenes críticas, prevención de layout shift, generación de srcset responsivo y soporte nativo para CDNs de imágenes populares. Lo notable es que muchas de estas optimizaciones requieren cero configuración más allá de reemplazar src por ngSrc.
Importar y activar la directiva¶
NgOptimizedImage vive en @angular/common y se usa como cualquier directiva standalone: importándola en el componente o en los imports del módulo.
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
@Component({
selector: 'app-portada-articulo',
standalone: true,
imports: [NgOptimizedImage],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<img
ngSrc="articulos/portada-angular-performance.webp"
width="1200"
height="630"
priority
alt="Portada del artículo sobre rendimiento en Angular"
/>
`
})
export class PortadaArticuloComponent {}
El cambio más visible es ngSrc en lugar de src. La directiva toma el control de cómo y cuándo se carga la imagen. width y height son obligatorios cuando no se usa fill: sin ellos la directiva lanza una advertencia en desarrollo porque sin dimensiones conocidas el navegador no puede reservar el espacio antes de que la imagen cargue, causando layout shift.
El atributo priority: imágenes LCP above the fold¶
priority es el atributo más impactante para el LCP. Cuando está presente, NgOptimizedImage genera un <link rel="preload"> en el <head> del documento para esa imagen, instruyendo al navegador para que la descargue con la máxima prioridad antes de procesar el resto del HTML. También desactiva el lazy loading para esa imagen.
La regla práctica es clara: cualquier imagen que sea el elemento más grande visible cuando la página carga por primera vez debe llevar priority. Solo una o dos imágenes por página deberían tenerlo -aplicarlo indiscriminadamente anula el beneficio.
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
@Component({
selector: 'app-hero',
standalone: true,
imports: [NgOptimizedImage],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Imagen hero: LCP → priority obligatorio -->
<img ngSrc="hero/banner-principal.webp" width="1440" height="600" priority
alt="Banner principal de la tienda" />
<!-- Imágenes de producto: lazy loading automático sin priority -->
@for (producto of productosDestacados; track producto.id) {
<img [ngSrc]="producto.imagenUrl"
width="300" height="300"
[alt]="producto.nombre" />
}
`
})
export class HeroComponent {
productosDestacados = [
{ id: 1, nombre: 'Teclado mecánico', imagenUrl: 'productos/teclado.webp' },
{ id: 2, nombre: 'Monitor 4K', imagenUrl: 'productos/monitor.webp' }
];
}
Las imágenes sin priority reciben automáticamente loading="lazy" y fetchpriority="low", lo que difiere su descarga hasta que están cerca del viewport. Esto es exactamente lo opuesto a lo que queremos para el LCP, y exactamente lo correcto para el resto de imágenes de la página.
fill: imágenes que llenan su contenedor¶
Cuando una imagen debe ocupar el 100% de su contenedor -carruseles, fondos de sección, galerías con grid CSS- usar width y height fijos no funciona bien. fill resuelve esto:
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
@Component({
selector: 'app-galeria-item',
standalone: true,
imports: [NgOptimizedImage],
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [`
.contenedor-imagen {
position: relative; /* obligatorio con fill */
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
}
`],
template: `
<div class="contenedor-imagen">
<!-- fill: imagen ocupa todo el contenedor - no necesita width/height -->
<img [ngSrc]="imagenUrl" fill [alt]="descripcion" />
</div>
`
})
export class GaleriaItemComponent {
imagenUrl = 'galeria/foto-destacada.webp';
descripcion = 'Foto destacada de la galería';
}
Con fill, el contenedor padre debe tener position: relative y dimensiones definidas. La imagen recibe internamente position: absolute; width: 100%; height: 100%. Es el equivalente de object-fit: cover para imágenes que deben adaptarse a cualquier tamaño de contenedor.
sizes: imágenes responsivas con srcset automático¶
NgOptimizedImage puede generar automáticamente un srcset con múltiples resoluciones si le indicamos cómo varía el tamaño de la imagen según el viewport:
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';
@Component({
selector: 'app-imagen-responsiva',
standalone: true,
imports: [NgOptimizedImage],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<img
ngSrc="productos/camara.webp"
width="800"
height="600"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
alt="Cámara mirrorless"
/>
`
})
export class ImagenResponsivaComponent {}
Con el atributo sizes, la directiva genera un srcset con variantes de la imagen para anchos de 640px, 750px, 828px, 1080px, 1200px, 1920px, 2048px y 3840px. El navegador elige automáticamente la variante más adecuada según el DPR y el tamaño real del elemento. Sin un loader de CDN configurado, esto requiere que los archivos de esas resoluciones existan en el servidor.
Loaders para CDNs de imágenes¶
La funcionalidad más poderosa de NgOptimizedImage aparece cuando se configura con un loader de CDN. El loader transforma la URL base de ngSrc en una URL real del CDN con los parámetros correctos para cada tamaño.
import { bootstrapApplication } from '@angular/platform-browser';
import { provideImageKitLoader } from '@angular/common';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
// Con ImageKit: las URLs se construyen automáticamente con transformaciones
provideImageKitLoader('https://ik.imagekit.io/mi-proyecto')
]
});
Con el loader de ImageKit configurado, ngSrc="productos/camara.webp" se convierte automáticamente en URLs como https://ik.imagekit.io/mi-proyecto/productos/camara.webp?tr=w-800,h-600 para el tamaño base, y variantes para cada punto del srcset. Angular también ofrece provideCloudinaryLoader, provideImgixLoader y provideNetlifyImageLoader con el mismo patrón.
Advertencias de desarrollo¶
En modo desarrollo, NgOptimizedImage es bastante vocal. Lanza advertencias en consola cuando detecta problemas comunes:
- Imagen sin
widthyheightque no usafill: potencial CLS. - Imagen con
priorityque no tiene<link rel="preload">en el<head>(posible cuando el componente carga tarde). - Imagen sin
priorityque ocupa más del 50% del viewport inicial: debería llevarpriority. - Diferencia entre las dimensiones declaradas y las dimensiones reales de la imagen: posible distorsión.
Estas advertencias desaparecen en producción, pero son valiosas durante el desarrollo para detectar configuraciones incorrectas antes de que afecten a los usuarios.
Puntos clave¶
- Reemplazar
srcporngSrcactiva optimizaciones automáticas: lazy loading,fetchpriority, generación desrcset priorityagrega<link rel="preload">y desactiva lazy loading; usar solo para la imagen LCP de cada páginafilles la alternativa awidth/heightcuando la imagen debe adaptarse al contenedor; requiereposition: relativeen el padresizespermite a la directiva generarsrcsetresponsivo automático con las resoluciones más comunes- Los loaders de CDN (
provideImageKitLoader,provideCloudinaryLoader, etc.) eliminan la necesidad de gestionar URLs de imagen manualmente
¿Qué sigue?¶
En la Parte 3 abrimos el bundle de la aplicación con webpack-bundle-analyzer y las herramientas de esbuild para identificar qué dependencias están engordando innecesariamente el código que descarga el usuario.