Photo by Irene Strong on Unsplash
How to Optimize Images in Angular Using ngOptimizedImage Directive
Improve Angular app load times with ngOptimizedImage Directive for large images
As developers, we create different types of applications. Sometimes, we work on data processing with forms or data visualization with grids or charts. But sometimes, we build applications that focus mainly on images. While making an app to show images might seem simple, do you know how to make sure it performs well?
When building an app focused on images, we need to learn about lazy loading, prioritization, and best practices like using CDNs and following web vitals such as layout shifts or addressing cumulative layout shift (CLS). This includes setting correct dimensions (width or height) and avoiding errors like using percentage dimensions in your CSS.
Yes, it seems like there's a lot to handle, but these points are crucial for building a good image-focused application. However, I have good news: Angular offers the ngOptimizedImage directive to make things easier. It helps you follow performance best practices without much effort, so you can focus on delivering your application while maintaining top performance.
The ngOptimizedImage directive is indeed a game changer in the way we build high-performance applications with Angular. Today, we'll see it in action with a real scenario. Let's get started.
Scenario
You have been hired to build an MVP focusing on Special Products. The page must display a list of products with brief descriptions on the homepage.
Here's the plan:
Create the application.
Retrieve the data using a service.
Develop a component to showcase each product along with its image.
At first glance, these tasks seem straightforward. Let's get started and build this project!
Set Up the Project
In your terminal, run the following command to create a new Angular project named 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-offers
Go to the products-offers
folder, edit the app.component.html, and remove the default HTML to be ready for our next step.
Now that our Angular project is ready, the next step is to get the data from the service!
Get the Data
Before creating the service to fetch product data via HTTP request, we need to configure the HttpClient
in the app.config
file.
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()
]
};
Next, create the service using the Angular CLI with the command 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)
Open the products.service.ts
file. First, inject the HttpClient
using the inject
function, and declare the API
variable with the endpoint. This will help us make the request to the 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)
}
To provide better typing, before the @Injectable
, declare a product type to return a strong type response with 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)
}
The final step is to expose the $products
data using the HttpClient and transform it into signals using the toSignal()
function.
The final code looks like this:
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));
}
Perfect, the service is ready. Let's display the products in the AppComponent.
Display the Products
Displaying the products is straightforward. Simply inject the productService
and utilize the signals $products
to seamlessly integrate them into the interface.
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;
}
In the HTML template, utilize the @for
directive to iterate through the products and include an <img>
tag along with the product description.
@for (product of $products(); track product) {
<img [src]="product.image">
<p>{{product.description}}</p>
}
Save changes. Instead of using ng serve
, let's build the application and serve it using another HTTP server to get a more realistic view of how the app will work.
Run the command npm run build
.
Navigate to products-offers\dist\products-offers\browser
and run the command npx local-web-server
to start a local web server.
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:8000
Go to http://127.0.0.1 and tada!! 🥳 The app is working!!!
Do you believe the app is finished and ready? Let's measure its performance and ensure it meets our standards.
The Performance
First, navigate again to http://127.0.0.1:8000 and open the Chrome Developer Tools. Click on the "Network" tab and reload the page.
😱 Oh no! It seems we're downloading all the images without the user needing to see them! The images amount to 2MB with 29 requests, taking 1.38 seconds. 😖
We'll continue the party by using the Lighthouse tool to analyze the app and see the results!
😵 We're only at 54% performance with a 4.8s LCP. 😿 Houston, we have a problem! These are not good numbers for our app. How can we fix this?
Well, I did mention the ngOptimizedImage directive. Let's implement it and see how our app improves.
The ngOptimizedImage Directive
The ngOptimizedImage directive help us to implement best practices for loading images and makes it easy to adopt performance best practices.
It helps to follow the Largest Contentful Paint (LCP) prioritization rule by automatically setting the fetchpriority
attribute on the <img>
tag for the most important image. We can then lazy load other images by default using a preconnect link tag in the document head using an automatically generated srcset
attribute.
Also we can do more stuff like use image CDN URLs to apply image optimizations and prevent layout shift by requiring width
and height
with a warning if width
or height
have been set incorrectly.
Now that we understand the benefits of the ngOptimizedImage directive, let's integrate it into our app.
Using the ngOptimizedImage Directive
Open the app.component.ts
to import the NgOptimizedImage
from @angular/common;
in the imports
section.
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;
}
Open the app.component.html file and change the src
attribute to ngSrc
. Additionally, implement default width
and height
attributes.
Some IDEs like WebStorm may display recommendations to set
width
andheight
.
Save changes and rebuild the app with the ng build
command.
Navigate again to http://127.0.0.1 and explore the network requests and Lighthouse results.
Good news!! 😃 We've made significant improvements to the performance:
Reduced the request to 5/14 request
Added Lazy Loading
Earned 97% performance in Lighthouse
Got LCP to 1.2 seconds
The ngOptimizedImage dDirective did a lot of magic for us by setting the loading and fetch priority and handling the automatic lazy loading of images. 🥳🎉
NgOptimizedImage helps us with even more functionalities, this is just a taste! I recommend checking out the official documentation.
Learn more about the image directive atangular.io/guide/image-directive.
Recap
We've taken our first steps in utilizing Chrome DevTools to analyze our app's performance. It's remarkable how effortlessly we can optimize our applications using the NgOptimizedImage directive, requiring only a few lines of code.