Optimizing Images with the Angular Image Directive
In May 2022, the Aurora and Angular teams announced that they would collaborate on an image directive for Angular. The directive was recently released for developer preview as part of Angular v14.2. This post talks about how the new image directive,
NgOptimizedImage, supports image optimization in Angular.
Images are a common and crucial component of web user experience, with 99.9% of web pages generating requests for one or more images. Images are also the most significant contributors to page weight, constituting a median of 982 kilobytes per page.
Due to their growing number and size, images can hinder the performance of web pages and affect Core Web Vitals metrics. For 79.4% of desktop pages, an image was the Largest Contentful Paint (LCP) element in 2021. The pursuit of optimized images has thus become a constant endeavor for many of us.
The Aurora team believes in leveraging the power of frameworks to provide baked-in solutions to common developer challenges. Their first foray into the image optimization space was the Next.js image component. They considered this component to be a testing ground for whether improving the developer experience (DX) of image optimization could lead to performance wins for more apps using frameworks.
The first set of results from Next.js user Leboncoin was encouraging. Leboncoin saw a significant LCP improvement (from 2.4s to 1.7s) after they started using
next/image. The subsequent adoption of
next/image in the community played a role in the increase of Next.js origins meeting LCP thresholds. Soon there were requests for similar features in other frameworks, one of them being Angular.
As a result, Aurora consulted with Angular and Nuxt to prototype image components for these frameworks. The Nuxt image component was released last year. Now the Angular image directive (
NgOptimizedImage) has been released to bring image optimization defaults to Angular.
Looking at the Core Web Vitals scores, the percentage of Angular origins that meet "good" LCP thresholds still needs work. Only 18.74% of Angular sites had good LCP on mobile in June 2022. Since images are the LCP element for more than 70% of the web pages on mobile and desktop, unoptimized LCP images could be one of the primary causes of poorer LCP on Angular websites.
The Angular image directive was designed to help improve these numbers.
MVP for the NgOptimizedImage directive
The MVP of the Angular image directive builds on lessons from the image components Aurora has built to date while adapting the design to Angular’s client-side rendering experience. Many of the standard image optimization problems have been addressed by either:
- Providing strong defaults.
- Throwing errors or warnings to ensure conformance to best practices.
The highlights of the design are as follows:
Intelligent lazy loading
Images that are invisible to the user on page load (for example, below-the-fold images or hidden carousel images) should ideally be lazy-loaded. Lazy loading frees up browser resources to load other critical text, media, or scripts. Most images are non-critical and should be lazy-loaded, but only 7.8% of pages used native lazy loading in 2021.
The Angular image directive lazy loads non-critical images by default and only eagerly loads images specially marked as
priority. This ensures that most images exhibit optimal loading behavior.
Prioritization of critical images
Adding resource hints (e.g.,
preconnect) to prioritize the loading of critical images is a recommended best practice. However, most apps are not using them. According to the 2021 Web Almanac, only 12.7% of mobile pages use preconnect hints and only 22.1% of mobile pages use preload hints.
The image directive acts on two fronts when images are marked as priority.
- It sets the fetchpriority of the image to
"high"so that the browser knows that it should download the image with a high priority.
- In development mode, a runtime check confirms that a
preconnectresource hint has been included corresponding to the image's origin.
In development mode, the directive also uses the PerformanceObserver API to verify that the LCP image has been marked
priorityas expected. If it's not marked
priority, an error is thrown, instructing the developer to add the
priorityattribute to the LCP image.
Ultimately, this combination of automation and conformance ensures that the LCP image has a
fetchpriorityattribute value of
high, and isn't lazy loaded.
- It sets the fetchpriority of the image to
Optimized configuration for popular image tooling
It's recommended that Angular applications use image CDNs, which often provide optimization services by default.
The directive encourages using image CDNs by providing an especially appealing developer experience (DX) to configure them in the app. It supports a loader API that allows you to define the CDN provider and your base URL in your configuration. Once configured, you only have to define the asset name in the markup. For example,
// in module providers:
// in markup
<img ngSrc="image.png" >
<img ngSrc="image2.png" >
This is equivalent to including the following image tags and reduces the markup developers must include for every image.
The image directive provides built-in loaders with optimal configuration for the most popular image CDNs. These loaders will format image URLs automatically to ensure that the recommended image format and compression settings are used for each CDN.
Built-in errors and warnings
In addition to the above built-in optimizations, the directive also has built-in checks to ensure that developers have followed the recommended best practices in the image markup. The image directive performs the following checks.
Unsized images: The image directive throws an error if the image markup does not have defined an explicit width and height. Unsized images can cause layout shifts, affecting the page's Cumulative Layout Shift (CLS) metric. The recommended best practice to prevent this is that images should have
Aspect ratio: The image directive throws an error to let developers know if the aspect ratio of the
heightdefined in the HTML is not close to the actual aspect ratio of the rendered image. This can cause the image to look distorted on screen. This can happen if
- You have defined the wrong dimensions (width or height) by mistake or
- If you have defined one dimension by percentage in your CSS, but not the other (for example,
height: autoto ensure the image grows in both dimensions).
Oversized images: If the image does not define a
srcsetand the intrinsic image is significantly larger than the rendered image, the directive will display a warning suggesting the use of the
Image density: The directive will throw an error if you try to include an image in the
srcsetwith a pixel density of more than
3x. Descriptors higher than
2xare generally not recommended because it has the unintended consequence of forcing high-resolution mobile devices to download huge images. Moreover, the human eye can't actually tell much of a difference above 2x.
Adapting image optimization strategies to work within a client-side framework was a primary challenge when designing
NgOptimizedImage. The default rendering experience on Next.js is Server Side Rendering (SSR) or Static Site Generation (SSG), while that on Angular is Client Side Rendering (CSR). Even though Angular supports an SSR library - angular/universal - most Angular apps (~60%) use CSR.
The image directive is entirely built for CSR to align with the typical use case in Angular apps. This set additional constraints, and the team had to rethink how to build specific optimizations for CSR apps.
Some of the challenges encountered are as follows:
Supporting resource hints
Preloading critical assets helps the browser discover them earlier. However, including resource hints in Angular apps is complicated because:
Manual Addition: It's difficult for developers to add the
preloadresource hint manually. Angular uses one shared index.html file for the entire project or for all routes in the website. Thus, the
<head>of the document is the same for every route (at least at serve time). Adding any
preloadhint to the
<head>would mean that the resource would be preloaded for all routes even where it is not required. Thus, the manual addition of
preloadhints is not recommended.
<head>will be rendered too late to be of any value.
For the first version of the directive, a combination of
fetchpriorityhints serve to prioritize the image in lieu of a
preload. However, Aurora is currently working with the Angular CLI team to enable automatic injection of resource hints at build time—stay tuned!
Optimizing image size and format on the server
As Angular apps are typically client-side rendered, images on the file system cannot be compressed at request time and are served as is. For this reason, using image CDNs is recommended to compress images and convert them into modern formats like WebP or AVIF on demand.
While the directive does not enforce the use of image CDN’s, it's strongly encouraged to use them with the directive and its built-in loaders ensure that the correct configuration options are used.
The following demo demonstrates the difference the Angular image directive can make to image performance. It compares two websites:
Website One: Uses native
<img> elements with images served through the Imgix CDN (with default configuration options).
Website Two: Use the image directive for all images. It also includes the optimizations recommended directly by warnings or errors thrown by the directive.
When running these demos through WebPageTest lab tests on a physical Moto G4 device, there is a substantial improvement of 56% in LCP. The original LCP of 4.5 seconds on Website One using native image tags reduces to 2 seconds on Website Two using the image directive.
The team worked with partners to validate the image directive's performance impact on real enterprise Angular applications.
One of these partners was Land's End. It was expected that their site would be a good test case for results that real applications might see.
Lighthouse lab testing was performed on their QA environment before and after using the image directive. On desktop, their median LCP decreased from 12.0s to 3.0s, a 75% improvement in LCP. On mobile, the median LCP decreased from 20.2s to 12.0s (40.6% improvement).
It's important to note that the lab testing above is only intended as a general demonstration of the potential of the directive. Field data is much preferred to validate performance impact. The Aurora team is currently working with a few production partners to attain this data, and the blog post will be updated as soon as it's available.
This is only the first installment of the design for the Angular image directive. There are many other features planned for future versions, including:
Better support for responsive images:
NgOptimizedImagecurrently supports using
srcset, but the
sizesattributes must be manually provided for each image. In the future, the directive could generate the
Automatic injection of resource hints
It might be possible to integrate with the Angular CLI to generate preconnect and preload tags for critical LCP images.
Support for Angular SSR
The MVP version is designed keeping in mind Angular CSR constraints, but it will also be important to explore image optimization solutions for Angular SSR (angular/universal).
Developer experience improvements
heightattributes are specified for each image. However, specifying these for each image may be tiresome for some developers. There is a potential to improve the developer experience here in the next iteration as follows:
The Angular image directive will be available to developers in stages, starting with the developer preview version in v14.2.0. Do give
NgOptimizedImage a try and leave feedback!
With special thanks to Katie Hempenius and Alex Castle for their contribution.