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 setwidth and height.

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.