Cómo optimizar imágenes en Angular con la directiva ngOptimizedImage

Como desarrolladores, creamos diferentes tipos de aplicaciones. A veces trabajamos en el procesamiento de datos con formularios o en la visualización de datos con tablas o gráficos. Pero en otras ocasiones, construimos aplicaciones enfocadas principalmente en imágenes. Aunque crear una aplicación para mostrar imágenes pueda parecer algo sencillo, ¿sabes cómo asegurarte de que tenga un buen rendimiento?
Al construir una aplicación centrada en imágenes, necesitamos aprender sobre carga diferida (lazy loading), priorización y buenas prácticas como usar CDNs y cuidar las métricas web esenciales (Core Web Vitals). Entre ellas están los cambios de diseño o layout shifts y el Cumulative Layout Shift (CLS). Esto incluye definir dimensiones correctas (width y height) y evitar errores comunes como usar porcentajes para las dimensiones en el CSS.
Sí, parece que hay muchos detalles que cuidar, pero estos puntos son fundamentales para crear una buena aplicación basada en imágenes. Por suerte, tengo buenas noticias: Angular incluye la directiva ngOptimizedImage para hacernos la vida más fácil. Esta directiva te ayuda a seguir las mejores prácticas de rendimiento casi sin esfuerzo, permitiéndote concentrarte en la funcionalidad de tu aplicación manteniendo una gran velocidad.
La directiva ngOptimizedImage marca una gran diferencia en la forma en que construimos aplicaciones de alto rendimiento con Angular. Hoy vamos a verla en acción con un caso real. ¡Empecemos!
El escenario
Imagina que nos contrataron para construir un MVP enfocado en productos especiales. La página principal debe mostrar una lista de productos con descripciones cortas.
Este es nuestro plan de trabajo:
-
Crear la aplicación.
-
Obtener los datos usando un servicio.
-
Crear un componente para mostrar cada producto junto con su imagen.
A primera vista, estas tareas parecen bastante directas. ¡Manos a la obra y armemos el proyecto!
Configurar el proyecto
En tu terminal, ejecuta el siguiente comando para crear un nuevo proyecto de Angular llamado products-offers:
ng new products-offers
? Which stylesheet format would you like to use? SCSS [
https://sass-lang.com/documentation/syntax#scss ]
? Do you want to enable Server-Side Rendering (SSR) and Static Site Generation
(SSG/Prerendering)? No
cd new products-offersEntra en la carpeta products-offers, abre el archivo app.component.html y borra todo el HTML por defecto para dejarlo listo para el siguiente paso.
Con nuestro proyecto de Angular preparado, el siguiente paso es obtener los datos desde un servicio.
Obtener los datos
Antes de crear el servicio para consultar los datos de los productos mediante una petición HTTP, necesitamos configurar HttpClient en el archivo app.config.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import {provideHttpClient} from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient()
]
};Luego, generamos el servicio con el CLI de Angular ejecutando el comando ng g s services/products.
C:\Users\DPAREDES\Desktop\products-offers>ng g s services/products
CREATE src/app/services/products.service.spec.ts (383 bytes)
CREATE src/app/services/products.service.ts (146 bytes)Abre el archivo products.service.ts. Primero, inyectamos HttpClient usando la función inject y declaramos la variable API con el endpoint. Esto nos servirá para hacer la petición a la API.
import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
@Injectable({
providedIn: 'root'
})
export class ProductsService {
private API = 'https://fakestoreapi.com/products'
http = inject(HttpClient)
}Para tener un mejor tipado, antes del @Injectable, declaramos el tipo Product para devolver una respuesta fuertemente tipada con httpClient.
import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
export type Product = {
description: string;
image: string
}
@Injectable({
providedIn: 'root'
})
export class ProductsService {
private API = 'https://fakestoreapi.com/products'
http = inject(HttpClient)
}El último paso es exponer los datos en $products usando el HttpClient y transformarlos en señales con la función toSignal().
El código final queda así:
import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {toSignal} from "@angular/core/rxjs-interop";
export type Product = {
description: string;
image: string
}
@Injectable({
providedIn: 'root'
})
export class ProductsService {
private API = 'https://fakestoreapi.com/products'
http = inject(HttpClient)
$products = toSignal(this.http.get<Array<Product>>(this.API));
}Listo, el servicio está terminado. Ahora pasemos a mostrar los productos en el AppComponent.
Mostrar los productos
Mostrar los productos es muy sencillo. Solo tenemos que inyectar productService y usar la señal $products para integrarlos directamente en la interfaz.
import {Component, inject} from '@angular/core';
import {ProductsService} from "./services/products.service";
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
$products = inject(ProductsService).$products;
}En la plantilla HTML, usamos la directiva @for para iterar a través de los productos y agregamos una etiqueta <img> junto con la descripción del producto.
@for (product of $products(); track product) {
<img [src]="product.image">
<p>{{product.description}}</p>
}Guarda los cambios. En lugar de usar ng serve, vamos a compilar la aplicación y servirla usando otro servidor HTTP para tener una perspectiva más realista de cómo funcionará la aplicación.
Ejecuta el comando npm run build.

Navega a products-offers\dist\products-offers\browser y ejecuta el comando npx local-web-server para iniciar un servidor web local.
C:\Users\DPAREDES\Desktop\products-offers\dist\products-offers> npx local-web-server
Need to install the following packages:
local-web-server@5.3.3
Ok to proceed? (y) y
Listening on http://INNO-D0H:8000, http://192.168.86.21:8000, http://127.0.0.1:8000Entra a http://127.0.0.1 y ¡listo! 🥳 La aplicación está funcionando.

¿Crees que la aplicación ya está terminada y lista? Midamos su rendimiento para asegurarnos de que cumpla con nuestros estándares.
El rendimiento
Primero, navega de nuevo a http://127.0.0.1:8000 y abre las Chrome Developer Tools. Haz clic en la pestaña "Network" y recarga la página.
😱 ¡Oh no! Parece que estamos descargando todas las imágenes sin que el usuario necesite verlas todavía. Las imágenes suman 2MB con 29 peticiones, tomando 1.38 segundos. 😖

Seguimos con el análisis usando la herramienta Lighthouse para evaluar la aplicación y ver los resultados:

😵 Apenas tenemos un 54% de rendimiento con un LCP de 4.8s. ¡Houston, tenemos un problema! No son buenos números para nuestra aplicación. ¿Cómo podemos solucionar esto?
Bueno, ya mencioné antes la directiva ngOptimizedImage. Vamos a implementarla para ver cómo mejora nuestra aplicación.
La directiva ngOptimizedImage
La directiva ngOptimizedImage nos ayuda a implementar las mejores prácticas para cargar imágenes y facilita adoptar estándares de alto rendimiento.
Nos ayuda a cumplir la regla de priorización de Largest Contentful Paint (LCP) configurando automáticamente el atributo fetchpriority en la etiqueta <img> para la imagen más importante. Además, aplica carga diferida (lazy loading) por defecto en las demás imágenes, sugiere etiquetas de preconexión en el encabezado del documento y genera automáticamente el atributo srcset.
También podemos hacer muchas más cosas, como usar URLs de CDN de imágenes para aplicar optimizaciones y evitar saltos de diseño (layout shift) haciendo obligatorios los atributos width y height, con advertencias si estos valores se configuraron de forma incorrecta.
Ahora que entendemos las ventajas de la directiva ngOptimizedImage, vamos a integrarla en nuestra aplicación.
Cómo usar la directiva ngOptimizedImage
Abre app.component.ts para importar NgOptimizedImage desde @angular/common en la sección de imports.
import {Component, inject} from '@angular/core';
import {ProductsService} from "./services/products.service";
import {NgOptimizedImage} from "@angular/common";
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
imports: [
NgOptimizedImage
],
styleUrl: './app.component.scss'
})
export class AppComponent {
$products = inject(ProductsService).$products;
}Abre el archivo app.component.html y cambia el atributo src por ngSrc. Además, define los atributos width y height por defecto.
Algunos IDEs como WebStorm pueden mostrar recomendaciones para definir
widthyheight.

Guarda los cambios y vuelve a compilar la aplicación con el comando ng build.

Navega nuevamente a http://127.0.0.1 y revisa las peticiones de red junto con los resultados de Lighthouse.

¡Buenas noticias! 😃 Logramos mejoras notables en el rendimiento:
-
Redujimos las peticiones a 5/14 peticiones
-
Añadimos Lazy Loading
-
Alcanzamos un 97% de rendimiento en Lighthouse
-
Bajamos el LCP a 1.2 segundos
La directiva ngOptimizedImage hizo un gran trabajo por nosotros al configurar las prioridades de carga, el fetch priority y manejar el lazy loading automático de las imágenes. 🥳🎉

NgOptimizedImage nos ayuda con todavía más funcionalidades, ¡esto es solo una pequeña muestra! Te recomiendo revisar la documentación oficial.
Aprende más sobre la directiva de imágenes en https://angular.io/guide/image-directive.
Resumen
Dimos nuestros primeros pasos usando Chrome DevTools para analizar el rendimiento de nuestra aplicación. Es notable lo sencillo que resulta optimizar nuestras aplicaciones usando la directiva NgOptimizedImage, necesitando apenas unas pocas líneas de código.
-
Código fuente: https://github.com/danywalls/using-ngOptimizeImage-angular
-
API de NgOptimizedImage: https://angular.io/api/common/NgOptimizedImage
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.