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.
https://medium.com/media/89ad8284096494d08b1970e011144944/href
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.
Running the DevTools Runtime Performance tool to inspect the LCP details and their preceding events.
In development mode, the NgOptimizedImage directive executes a detection mechanism relying on a PerformanceObserver 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.
Browser console reporting a runtime error if an LCP image is not declared as a priority.
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.
Rendering the code example without the `sizes` attribute generates a `srcset` attribute with image routes matching the corresponding sizes to the default pixel density descriptors.
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.
https://medium.com/media/9e02bd51b36669de616ed661737ef457/href
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.
Browser console reporting a runtime error on invalid `ngSrcset` value.
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.
Browser console reporting a runtime error due to an unreasonably high 4x pixel density descriptor mentioned in the `ngSrcset` attribute value.
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 run-time 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.
Browser console warning triggered by a missing domain preconnect link for a priority image.
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.
https://medium.com/media/94e3278118a88f8b30fbb231c081f75d/href
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.
Waiting for the high-res image to load after including the `placeholder` attribute to the element controlled by the NgOptimizedImage directive.After fetching the suitable image from the `srcset` list, the temporary CSS styles supporting the low-res placeholder are removed from the image element.
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.
Browser console reporting a runtime error if the `placeholder` attribute is used with a generic loader.
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.
Browser console reporting a runtime error on very large base64 encoded image lengths.
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:
https://medium.com/media/09fef9c41be5d0a5d91cba3d305e1e96/href
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:
Lee la versión en español de este artículo en el blog de ng-conf:
Ajustes automáticos en las imágenes optimizadas por Angular
Automatic Adjustments in Angular’s Optimized Images was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.