Automatic Adjustments in Angular’s Optimized Images
How the NgOptimizedImage directive streamlines image rendering performance
In the realm of web development, optimizing images for appropriate rendering is crucial. This article highlights the built-in performance adjustments offered by the NgOptimizedImage
directive, a powerful feature within the Angular framework that seamlessly enables browsers to anticipate which images to prioritize or defer, assists in responsive images setup, and automatically configure image placeholders, improving the perceived loading speed of web content for our users.
Let's activate the NgOptimizedImage
directive by importing it from the @angular/common
package and replacing the image element’s src
attribute on the code example with ngSrc
. This substitution hands control over to the directive logic, which generates during compilation the resulting <img>
with links to images that are selectively loaded by the browser according to user’s device capabilities and network conditions.
In the example, we retrieved image data from Unsplash, the photo stock library adopted officially by the Medium publishing platform, using a custom image loader.
Opting for a custom or a built-in image loader—which provides support for 3rd-party image services such as Cloudflare, Imgix, and Netlify—over a generic loader without URL transformation, enables further optimization through CDN-specific features like image resizing, format selection, and output quality.
Here’s the resulting HTML of the Angular component template after compilation in production mode:
<app-optimized-unsplash-image>
<article>
<img
alt="Optimized Unsplash Image"
ngsrc="0*hiSjMcbdqbV35wRI"
width="600"
height="400"
decoding="sync"
priority=""
loading="eager"
fetchpriority="high"
ng-img="true"
src="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
srcset="https://cdn-images-1.medium.com/max/600/0*hiSjMcbdqbV35wRI 1x,
https://cdn-images-1.medium.com/max/1200/0*hiSjMcbdqbV35wRI 2x">
</article>
</app-optimized-unsplash-image>
Optimizing image loading
The NgOptimizedImage
directive uses a priority
attribute to mark critical images for processing, enhancing their load time to improve web performance indicators like the LCP Web Vital.
This metric measures how fast the browser is able to render the largest visible element having text, image or video content. It is one of the culminating steps of the Critical Rendering Path that gives us confidence that our users will get relevant content promptly.
This loading sequence starts when the browser starts fetching and parsing the HTML document to convert the markup and style rules into logical structures like the DOM and the CSSOM trees. Alongside, parsed JavaScript code is being compiled and interpreted, manipulating these trees before style and layout calculations are executed as efficiently as possible to be able to paint the resulting web elements on the screen.
The DevTools Performance panel can help us visualize and inspect any bottlenecks to optimize the rendering sequence and reach the LCP event as early as possible.
In development mode, the NgOptimizedImage
directive executes a detection mechanism relying on a PerformanceObserver
that watches for LCP events. Suppose the LCP element happens to be an Angular-optimized image missing the priority
attribute. In that case, the browser console displays a runtime error alerting developers to tag the image appropriately for better web performance outcomes.
The directive then can automatically adjust the image loading behavior based on the presence of the priority
attribute. Images with this instruction get the fetchpriority="high"
and the loading="eager"
attribute values applied, indicating the browser to retrieve and render them as soon as their HTML node element is processed.
The remaining images—considered non-essential—will get the loading="lazy"
attribute value applied, allowing the browser to focus on rendering more critical content first. This browser’s native strategy (introduced in Chromium-based browsers in 2019 and fully available across all modern browsers and platforms since 2022) defers the loading of these images until they are likely needed—for instance, when they are nearly in view as a user scrolls or clicks towards them.
Note that the decoding="sync"
attribute value was manually added to the priority image as an extra hint to the browser to decode it alongside other processing tasks. Conversely, for extra performance enhancement in lazy-loaded images, we can apply the decoding="async"
attribute value to allow other content to render before the image data from a compressed format gets decoded prior to the painting step to bring pixels to the screen.
We can also notice that images processed by the NgOptimizedImage
directive are all marked with an ng-img
attribute, so we can accurately identify them during synthetic testing or real user monitoring and measure their true impact on the overall app performance.
Building a responsive image
Another attribute the NgOptimizedImage
directive automatically generates an image srcset
when a custom or 3rd-party loader is used, listing candidate images for browsers to render under specific conditions, such as varying viewport dimensions or the device screen pixel density. This native browser feature not only potentially reduce image download size but also supports crafting responsive designs, optimizing user experiences across different devices.
If the sizes
attribute is not present as in the above example, a fixed srcset
will be calculated using the default 1x and 2x pixel density descriptors, and the info obtained from the width
attribute and the config.width
parameter used in the image loader.
For enhanced art direction, it’s beneficial to specify the sizes
attribute with values that reflect the expected image width for each media condition targeted. The NgOptimizedImage
directive defaults to sixteen breakpoints, so providing our own list based on our app’s design can reduce the length of the automatically generated srcset
values to our specific requirements.
The compiled HTML output now shows a tailored srcset
generated by the directive using the sizes
information.
<app-optimized-unsplash-image>
<article>
<img
alt="Optimized Unsplash Image"
ngsrc="0*hiSjMcbdqbV35wRI"
width="600"
height="400"
decoding="sync"
sizes="(max-width: 400px) 100vw,
(max-width: 1200px) 50vw,
100vw"
priority=""
loading="eager"
fetchpriority="high"
ng-img="true"
src="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
srcset="https://cdn-images-1.medium.com/max/380/0*hiSjMcbdqbV35wRI 380w,
https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI 640w,
https://cdn-images-1.medium.com/max/1200/0*hiSjMcbdqbV35wRI 1200w,
https://cdn-images-1.medium.com/max/1920/0*hiSjMcbdqbV35wRI 1920w,
https://cdn-images-1.medium.com/max/2048/0*hiSjMcbdqbV35wRI 2048w,
https://cdn-images-1.medium.com/max/3840/0*hiSjMcbdqbV35wRI 3840w">
</article>
</app-optimized-unsplash-image>
If we need to provide image candidates using pixel density descriptors, the NgOptimizedImage
directive allows us to enter in a ngSrcset
input a list of either width or pixel density descriptors, but not a combination of both. Attempting to declare combined values like ngSrcset="400w, 600w, 1200w, 1x, 2x"
will throw a runtime error.
Furthermore, passing very high values of pixel density descriptors throws an error in development mode. The directive is designed to support up to a maximum of 3x density, with a recommendation to not exceeding 2x based on the capabilities of the human visual system. Higher values results in unnecessary file size increase without a perceptible enhancement in image clarity for the user, leading to slower load times.
If we need fine-grained control over image selection due to complex media conditions, opting out of automatic srcset
generation when using image loaders is an option. This can be easily done by setting the disableOptimizedSrcset="true"
attribute in the optimized image tag.
Generating speculative loading hints
As developers, we have the opportunity to inform browsers about which images are candidates for earlier processing requests before their rendering conditions are met.
The Angular browser builder performs a quick search to find domains listed in the image loader. Upon detecting a match that hasn’t been manually declared yet, a preconnect link is added to the document head, with a data-ngimg
attribute indicating that the <link>
element was automatically included during build time.
<head>
<!-- Additional elements in the <head> section -->
<!-- Automatically appended preconnect link to the Medium CDN -->
<link
rel="preconnect"
href="https://cdn-images-1.medium.com"
data-ngimg>
</head>
This form of speculative loading indicates the browser that it should prioritize the DNS lookup, TLS negotiation, and TCP handshake with the image server likely to serve the LCP element.
Additionally, in development mode, there’s a runtime preconnect link checker that verifies if the corresponding hint exists for optimize images marked with the priority
attribute. When the checker cannot confirm this, a warning is displayed in the browser console.
We can inform the link checker about domains dedicated to development and testing that don’t require preconnect hints by specifying them in the PRECONNECT_CHECK_BLOCKLIST
token to disable undesired warnings. By default, the addresses localhost
, 127.0.0.1
, and 0.0.0.0
are included in this blocklist.
{
provide: PRECONNECT_CHECK_BLOCKLIST,
useValue: [
'https://dev-domain.com',
'https://test-domain.com',
'https://another-excluded-domain.com'
]
}
When the Angular app is delivered using SSR (Server-side Rendering), the directive adds a preload link element to the document head. This hint goes beyond preconnecting by asking the browser to initiate both early connection and data download of the image, enhancing the user’s perception of load speed thought the prioritized loading of candidate LCP elements.
<head>
<!-- Additional elements in the <head> section -->
<!-- Automatically appended preload link to the prioritized image on SSR mode -->
<link
as="image"
href="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
rel="preload"
fetchpriority="high">
</head>
Responsive images have the imagesizes
and imagesrcset
attributes added to the <link>
element when the corresponding sizes
or srcset
attributes are declared in the optimized image.
<head>
<!-- Additional elements in the <head> section -->
<!-- Automatically appended preload link to the prioritized responsive image on SSR mode -->
<link
as="image"
href="https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI"
rel="preload"
fetchpriority="high"
imagesizes="(max-width: 400px) 100vw,
(max-width: 1200px) 50vw,
100vw"
imagesrcset="https://cdn-images-1.medium.com/max/380/0*hiSjMcbdqbV35wRI 380w,
https://cdn-images-1.medium.com/max/640/0*hiSjMcbdqbV35wRI 640w,
https://cdn-images-1.medium.com/max/1200/0*hiSjMcbdqbV35wRI 1200w,
https://cdn-images-1.medium.com/max/1920/0*hiSjMcbdqbV35wRI 1920w,
https://cdn-images-1.medium.com/max/2048/0*hiSjMcbdqbV35wRI 2048w,
https://cdn-images-1.medium.com/max/3840/0*hiSjMcbdqbV35wRI 3840w">
</head>
We can take a leap ahead by manually including our own rules in a <script type="speculationrules">
element, following the experimental Speculation Rules API. They give us granular control over prefetching and prerendering strategies, leveraging browser idle time and network resources. They also avoid prefetching in battery and data saver modes, and help to protect data privacy during cross-site prefetching.
Including image placeholders
Angular 17.2 introduced automatic placeholders for images, requiring a very minimal setup by adding the placeholder
attribute to the optimized image element. When specified, the directive requests a tiny version of the image, presenting it as a temporary blurry placeholder to create a smooth transition effect until the high-resolution image is fully loaded.
The default placeholder’s width is 30px, adjustable if needed by setting a custom placeholderResolution
property value in the directive’s IMAGE_CONFIG
provider.
The NgOptimizedImage
directive applies a few inline CSS rules to its host element to conditionally stretch the low-resolution placeholder and cover the entire container area with it as a background image. An optional CSS filter: blur(...)
property is used to blur the pixelated thumbnail.
Let's take into consideration that when the placeholder
attribute is set but no image loader is provided, the directive risks declaring the higher resolution image as its own placeholder, leading to unnecessary style transformations and re-paints.
In the development mode, a runtime error is thrown if this condition is met, advising us to fix this issue.
Anyway, the directive offers a way to display a custom placeholder when using a generic loader, by allowing the passing of a reasonably-sized base64 encoded thumbnail into the placeholder
attribute. Using less than 4,000 characters should be sufficient to encode a low-res image of 20 x 20 pixels. Exceeding this length generates a warning in the browser console during development mode. To prevent an unintended increase in HTML template size that can undermine our performance enhancement efforts, a runtime error is thrown if the length of the encoded image goes beyond 10,000 characters.
It’s worth noting that the concept of placeholder at the optimized image level is not connected to the ones within deferrable views, as the @placeholder
sub-blocks operate until their deferred conditions are satisfied. However, we can make them complement each other, as suggested in the following template code example:
Outro
As we’ve explored, proper usage of the NgOptimizedImage
not only enables the directive to efficiently carry out internal automation that help the the browser make informed decisions on rendering images, but also enhances overall application performance and UX.
Check out the following GitHub repository with the code referenced in this article to continue experimenting on your own how Angular generates the HTML markup for optimized image elements: