Summarize with AI:
In this guide, we will see how to use Angular’s NgOptimizedImage directive to address some common image optimization concerns effortlessly.
There is hardly a website you will come across that does not have images, and this shows how indispensable they are in driving sales, enhancing credibility and improving user engagement.
While this is good news, on the other hand, images contribute to 42% of why websites are slow, as they take up significant bandwidth and are a major contributor to why websites do not meet the standard requirements for performance on the internet, as defined by Google’s Core Web Vitals and its LCP metric.
This raises the important question of how best to take advantage of images. The answer is image optimization—a set of techniques that enable web developers to effectively use images and deliver the best user experience.
Framework developers, having understood the difficulties developers face while using images, have created tools to do the heavy lifting. In this article, we will go through the use of Angular’s NgOptimizedImage directive, which builds on top of the traditional <img/> element with some enhancements. We will see how it can be used to address some common image optimization concerns effortlessly.
This section will discuss a set of areas that require attention to get it right with images, irrespective of the platform on which images are used. The browser provides two main tags to work with images: <picture/> and <img/>, with the latter being the preferred option.
As we go over these areas and the techniques used, where necessary we will also specify attributes provided by the img element that can be used to solve these problems.
We will split our optimization concerns into three categories:
Optimizing a single image involves the following:
Different image formats provide different compression levels and are best suited for different purposes:
Devices have different resolutions and pixel densities/ratios. Developers need to serve the right image to the right device.
Device pixel ratio, in simple terms, refers to how many of the device’s physical pixels can fit into the dimensions of a CSS pixel. So a device with a higher DPR should be served a high-resolution image and vice versa.

The img element ships with two main attributes to solve this problem. The srcset attribute allows developers to configure the candidate images to enable browsers to select the right one.
When working with multiple images, optimization concerns involve the following:
Lazy loading means that only the required image(s) are presented to the user, which saves bandwidth and reduces the page’s critical rendering path, which in turn lowers its load time. The img tag ships with the loading attribute, which can be set to “lazy” to achieve this behavior.
Low-quality image placeholders should be used in conjunction with lazy loading to minimize the perceived loading time while the browser downloads the actual image. For local images, developers may need to generate placeholders manually or use specialized tools. When using a remote provider, some JavaScript is still required to display the placeholder before the browser downloads the actual image.
Not all images on a page are of equal importance (e.g., LCP—Largest Contentful Paint—images). Developers should prioritize more important images over less important ones. The img tag provides the loading and fetchPriority attributes to solve these issues, together with preloading the image using the link element.
Optimizing a standard image involves the following:
The LCP of a webpage is a measure of how long it takes to display its main content—it could be an image, a section, a video, etc. A good LCP is within the range of 0-2.5s.
When the LCP element on a page is an image, developers should load that image to the webpage as soon as possible while also verifying that the image size is optimized. The img element provides us with loading and fetchPriority attributes. The link element can also be used to prioritize fetching important assets, such as images on the webpage.
Layout shifts occur when there is a sudden movement of one or more elements on the webpage because something changed or was introduced to the DOM while the webpage was still loading. With images, it occurs when the browser loads the image at a later time after loading some of the other content on the page, and when space was not reserved for the image. So after the image is added to the DOM, other elements are displaced.
When images are not used as background images, it is essential to define a width and height for the image so that the space is reserved for the image, even when the load time is delayed.
We have discussed the different things we need to optimize when using images, and those that we can resolve using combinations of one or more attributes provided by the img element. One might wonder why we still need the NgOptimizedImage directive. Let’s go over a few points.
The NgOptimizedImage directive builds on top of the img element and enhances it in the following ways:
img element, so that developers do not have to remember to use them and can focus on building their applications.width and height when images are not used as background images, so that layout shifts do not happen when loading images.Let’s now see this directive in action. We will revisit the image optimization areas we listed earlier and see firsthand how to use the NgOptimizedImage directive in code. We will see how each step can be achieved with local and remote images.
Run the command below to clone the starter project:
https://github.com/christiannwamba/ng-image-optimization
Our starter application has a few local images in its /public folder.
Apart from the root app component, the demo application defines three other components:
local.component.ts: The component in this file will show how to use local images.remote-image.component.ts: The component in this file will show how to use remote images.local-and-remote-image.component.ts: We will update this component later to see how to use local and remote images in one component using custom loaders.Let’s start by showing the basic use of this directive.
Update your local.component.ts file with the following:
import { Component } from "@angular/core";
import { NgOptimizedImage } from "@angular/common";
@Component({
selector: "local-use",
imports: [NgOptimizedImage],
template: `
<h3>Local image</h3>
@for (img of localImages; track $index) {
<div style="padding-top: 700px">
<img width="800" height="600" [ngSrc]="img" />
</div>
}
`,
styles: ``,
})
export class LocalImageComponent {
localImages = [
"2.webp",
"3.webp",
"4.jpg",
"5.jpg",
"6.jpg",
"7.jpg",
"8.jpg",
"9.jpg",
];
}
Update your remote-image.component.ts file with the following:
import { NgOptimizedImage, provideCloudinaryLoader } from "@angular/common";
import { Component } from "@angular/core";
@Component({
selector: "remote-image-use",
imports: [NgOptimizedImage],
providers: [
provideCloudinaryLoader("https://res.cloudinary.com/chrisnwamba1"),
],
template: `
<h3>Remote images</h3>
@for (img of remoteImages; track $index) {
<div class="">
<img width="800" height="600" [ngSrc]="img" />
</div>
}
`,
styles: ``,
})
export class RemoteImageComponent {
remoteImages = [
"jcuh90sdncxwugdsqh40",
"qowugcszjxcdixvalvcp",
"err9wujrz1epkrhjbjtz",
"fypu0o7ea3ssvu4m97uh",
];
}
We start by importing the NgOptimizedImage directive and injecting it into the imports array.
In each component, we defined an array of local and remote images in the localImages and remoteImages variables, respectively.
Since we want our remote images to be managed for us by a third-party provider, Cloudinary, in our case, we created a Cloudinary account and uploaded a few images there.
Feel free to use your preferred media provider solution; we just chose to go with Cloudinary in this article.
To work with remote images, we make use of loaders, which are just functions that resolve to URLs. We can create custom loaders or use those that are supported by default. We used the Cloudinary loader by injecting provideCloudinaryLoader, which we fed the base URL pointing to our cloud name.
We used the ngSrc property to set the image source. It is mandatory to define a width and height for images; this helps prevent layout shifts if the image is delayed during loading. Failure to do so will likely result in errors.

You can start your application server by running npm run start. If you go ahead and inspect the img elements, you will notice that both local and remote images are lazy-loaded by default, as indicated by the loading="lazy" attribute, and there is no priority assigned to any image as specified by fetchPriority="auto".


By default, the srcset attribute is added for remote images, and it defines candidate images for different DPRs.

Just by using the ngSrc attribute, the benefits of the NgOptimizedImage directive are immediately obvious. Let’s now proceed to make some tweaks in our app to address the following areas:
Update the local.component.ts file with the following:
@Component({
selector: 'local-use',
imports: [NgOptimizedImage],
template: `
<div
style="
width: 300px;
height: 700px;
overflow: hidden;
position: relative;
margin-inline: auto;
margin-top:700px;
padding:30px;
"
>
<img
fill
[ngSrc]="localImages[3]"
style="object-fit: cover; z-index: -1"
/>
<H1>Hello</H1>
<p>testing Backgrounds</p>
</div>
By using the fill attribute, we can use images as backgrounds. This property overlays the image in its parent container using CSS. The fill attribute does not require us to specify a width or height property.
Here is a sample output:

<h3>Local image</h3>
<img
width="800"
height="600"
[ngSrc]="localImages[3]"
placeholder="data:image/webp;base64,UklGRtAAAABXRUJQVlA4IMQAAABwBQCdASoeABYAPn04lEeko6IhN/qoAJAPiWUAtOmMvNuEG3k3qonsrAxlVBwKRSuctL6AAP5RW9WtEC0SDum4fF68ZQfeqBp3ieyso2Hc2ynsYXNcHAVa/DMTVvEUDdP0bRJtvmL3wvsyJ806MJRwREZAAVSaRrRK08CE6p8y76jW+EO/e7rKmFK5yUhVi3z7XYbOSbwkJ+X7ferG15xGCnyYvNIHoozVzX9IOCk0RxXcw5ZqEee3QgU+m05NtntkAAAA"
/>
For local images, the image placeholder property must be manually set to a data URL as shown above. It is recommended that data URLs should be kept within 4KB in size. You can use tools like FFmpeg or visit websites like blurred.dev to generate placeholders. Failure to use a data URL will generate an error.

@for (img of remoteImages; track $index) {
<div class="">
<img
width="800"
height="590"
[ngSrc]="img"
placeholder
/>
</div>
}
For remote images, just by setting the placeholder property, the loader generates a placeholder URL for all our images.

By default, placeholders have a width of 30 px in the generated URL as shown in the image below:

We can customize the width of placeholders by using the IMAGE_CONFIG injection token. This is only applicable when using remote images.
import { IMAGE_CONFIG, NgOptimizedImage, provideCloudinaryLoader } from '@angular/common';
import { Component } from '@angular/core';
@Component({
selector: 'remote-image-use',
imports: [NgOptimizedImage],
providers: [provideCloudinaryLoader('https://res.cloudinary.com/dqydioa17'),
{
provide: IMAGE_CONFIG,
useValue: {
placeholderResolution: 40,
}
},
],
If you set the placeholderResolution to 40, you’ll notice that the request URL is updated as shown below:

By default, placeholder images are blurred out. As we saw earlier, the placeholderConfig property is an object that allows us to enable or disable the blur effect. It accepts an object with only one property called blur and can be used for both remote and local images.
@for (img of remoteImages; track $index) {
<div style="padding-bottom: 900px">
<img
width="800"
height="590"
[ngSrc]="img"
placeholder
[placeholderConfig]="{blur:false}"
/>
</div>
}
Websites may use one or more images, depending on the use case. Some images may need to be loaded earlier than others because an image could be the LCP element on the page. Hence, the need to give certain images a higher loading priority over others.
@for (img of remoteImages; track $index) {
<div style="padding-bottom: 900px">
<img
width="800"
height="590"
[ngSrc]="img"
placeholder
[placeholderConfig]="{blur:false}"
[priority]="$index ===2"
/>
</div>
}
For demonstration purposes, we set the priority property for the third image to true, simply assuming it is our LCP image.

As seen above, this sets the loading and fetchPriority to eager and high respectively.

As seen above, the image with higher priority is loaded before other resources. For the img element, it sets the loading and fetchPriority attributes on the image to eager and high respectively, so that the browser downloads the image as soon as possible and prioritizes it over any other resources being loaded.

Since we also enabled server-side rendering in our project, a link element is injected in the head section of the page. The rel="preload" instructs the browser to load the given asset (the image) as soon as possible.
As stated earlier, optimizing images for quality and resolution involves two steps:
srcset attributesizes propWe already established earlier that simply setting the ngSrc prop, the srcset attribute is populated. Different candidates are generated for device pixel ratios/densities in the srcset field, as shown below:

Another way to look at it is that by default, ngSrcset="1x, 2x". The ngSrcset attribute allows us to define candidate images for the browser to choose from. It takes a comma-separated list of DPRs or width descriptors.
Density descriptors or DPR can only be one of 1x, 2x, or 3x; anything outside these will result in an error. Width descriptors are a comma-separated list of numbers with a w suffix (e.g., 200w, 700w, 900w), which can be thought of as 200 px, 700 px and 900 px. However, px is not used.
The ngSrcset attribute allows us to customize the candidate image URLs to be populated in the img element’s srcset attribute using a shorter syntax. Let’s update the local.component.ts file to see it in action:
@for (img of remoteImages; track $index) {
<div style="padding-bottom: 900px">
<img
width="800"
height="590"
[ngSrc]="img"
placeholder
[placeholderConfig]="{blur:false}"
[priority]="$index ===0"
ngSrcset="200w, 700w, 900w"
/>
</div>
}
This tells the browser to generate three URLs at 200 px, 700 px and 900 px. When we look at the DOM, we see that the srcset attribute now defines three images based on the specified sizes, as shown below:

When we define selection rules using the sizes prop, we instruct the browser how to select the candidate image in the srcset attribute based on available space.
@for (img of remoteImages; track $index) {
<div style="padding-bottom: 900px">
<img
width="800"
height="590"
[ngSrc]="img"
placeholder
[placeholderConfig]="{blur:false}"
[priority]="$index ===0"
sizes="(max-width: 30em) 100vw"
/>
</div>
}
In the code above, we stated that if the viewport size is less than 30em, the browser should display the image that is 100vw.
To put it into context, assuming 30em maps to 480 px, if the width of the device is less than or equal to 480 px, then the image to be displayed is the one at 100vw.
If 100vw for the target device maps to, say, 300 px, the browser looks at the list of images specified in the srcset and renders the one equal to or greater than, but closest to, 300 px. So if there were images of 900w, 650w and 1200w, the one at 650w is selected.
Specifying sizes informs the NgOptimizedImage directive that we want width descriptors. Since we didn’t specify any above, like we did with ngSrcset previously, it automatically populates the srcset attribute with the default set of width descriptors it maintains, as shown below:

To customize the generated widths, we can either define an ngSrcset per image, as we did, or define them globally using the IMAGE_CONFIG injection token.
providers: [
{
provide: IMAGE_CONFIG,
useValue: {
placeholderResolution: 30,
breakpoints: [500, 600, 1000]
}
},
When we refresh the application and inspect the DOM now, we notice that the srcset fields have been updated.

We already established that loaders are functions that receive some parameters and resolve a URL based on some predefined rules. So far, we have been using one of the default loaders (provideCloudinaryLoader), and this works well. However, there are situations when we have more complex requirements in our app that would require us to create a custom loader to achieve our goals.
Here is a list of things we want to support in our app and why we will be needing one:
Some other reasons why you might need to define an image loader include:
All the code we will write here will be in the local-and-remote-image.component.ts file. Update the file to look like so:
import {
IMAGE_LOADER,
ImageLoaderConfig,
NgOptimizedImage,
} from "@angular/common";
import { Component } from "@angular/core";
@Component({
selector: "local-and-remote-image",
imports: [NgOptimizedImage],
providers: [
{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
if (config && config.loaderParams && config.loaderParams["local"]) {
return "/" + config.src;
}
const deliveryType =
(config.loaderParams && config.loaderParams["delivery_type"]) ||
"upload";
let url = `https://res.cloudinary.com/dqydioa17/image/${deliveryType}`;
if (config.loaderParams && config.loaderParams["tf"]) {
url += config.loaderParams["tf"];
}
if (config.width) {
url += `/w_${config.width}`;
}
url += `/${config.src}`;
return url;
},
},
],
template: `
<div style="padding-top: 50px">
<h2>local image</h2>
<img
width="800"
height="600"
[ngSrc]="'8.jpg'"
[loaderParams]="{ local: true }"
/>
</div>
<div style="padding-top: 50px">
<h2>remote image with effects</h2>
<img
width="800"
height="600"
[ngSrc]="'jcuh90sdncxwugdsqh40'"
placeholder
[loaderParams]="{ tf: '/e_enhance' }"
/>
</div>
<div style="padding-top: 50px">
<h2>remote image with effects</h2>
<img
width="600"
height="700"
[ngSrc]="
'https://upload.wikimedia.org/wikipedia/commons/1/13/Benedict_Cumberbatch_2011.png'
"
[loaderParams]="{ delivery_type: 'fetch', tf: '/e_vignette' }"
/>
</div>
`,
styles: ``,
})
export class LocalAndRemoteImageComponent {}

Our new component shows how we can use a custom loader to do amazing things. This component displays three images: a local one and two remote images with some effects applied. It starts by defining a custom loader, which is just a function bound to the IMAGE_LOADER injection token, which accepts an object that looks like so:
interface ImageLoaderConfig {
src: string;
width?: number;
isPlaceholder?: boolean;
loaderParams?: {
[key: string]: any;
};
}
Of all these properties, the one we tinkered with most is the one called loaderParams, which allows us to define arbitrary key-value pairs to enable our loader to effectively decide how to resolve the URL. For the local image, we defined a local property.
For the remote images, we defined two:
tf: This holds image effects as defined in the Cloudinary docsdelivery_type: Setting this to fetch means that the image is first fetched remotely, then added to our remote assetsImages are an integral part of the web. This guide shows how to use images effectively. Even though our focus was on the Angular framework, there are still a lot of takeaways to apply when using images in future projects.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.