Angular NgOptimizedImage: Faster LCP With One Directive [2026]

Link copied
Angular NgOptimizedImage: Faster LCP With One Directive [2026]

Angular NgOptimizedImage: Faster LCP With One Directive [2026]

On most content and e-commerce pages, the largest element on the first screen is an image: a hero banner, a product photo, a header illustration. That makes the image the thing Google's Largest Contentful Paint (LCP) measures, and a slow LCP is the most common reason an Angular page fails Core Web Vitals. The fixes are well known: load the hero early, load everything else late, serve the right size, and reserve space so nothing jumps. Angular packages all of them into one directive, NgOptimizedImage.

This is lesson 8.4 of the Angular Tutorial. Lessons 8.1 to 8.3 were about rendering work; this one is about loading. You'll learn what the directive does, how to mark the LCP image, how sizing prevents layout shift, how responsive srcset and image CDNs fit in, and how to migrate existing <img> tags safely.

What the directive does for you #

Problem What NgOptimizedImage does
Hero image discovered late priority sets fetchpriority="high" and loading="eager", and adds a preload link when server-rendered
Below-the-fold images compete for bandwidth Non-priority images default to loading="lazy"
Layout shift as images load Requires width and height (or fill) so the browser reserves space
Oversized downloads on small screens Generates a srcset so the browser picks the right size (with an image loader)
Mistakes nobody notices Dev-mode warnings for a missing priority, wrong dimensions, distorted aspect ratios and missing preconnects

Basic usage #

Import the directive and replace src with ngSrc:

import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-product-hero',
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="/images/headphones.jpg" width="1200" height="800" alt="Wireless headphones on a desk" priority />
  `,
})
export class ProductHero {}

width and height are the image's intrinsic dimensions (the file's pixel size), not how big it's displayed. CSS still controls the rendered size; the attributes let the browser compute the aspect ratio and reserve space before the file arrives. That's what prevents Cumulative Layout Shift (CLS).

Attribute cheat sheet #

Attribute Required? What it's for
ngSrc Yes Replaces src; activates the directive (a path relative to the loader, if you use one)
width / height Yes, unless fill Intrinsic pixel size; reserves space and fixes the aspect ratio
fill Instead of width/height Image fills its positioned parent; for unknown sizes and covers
priority On the LCP image High fetch priority, eager loading, preload link with SSR
sizes For responsive images Tells the browser how wide the image renders, so it picks the right srcset entry
loading Rarely Override the lazy default (eager, lazy, auto)
placeholder Optional Blurred low-res preview while loading (needs a loader, or a data URL)
disableOptimizedSrcset Rarely Turn off srcset generation for one image

Mark exactly one LCP image as priority #

priority is the single most valuable attribute:

<!-- The hero: load it first -->
<img ngSrc="/images/hero.jpg" width="1600" height="900" alt="…" priority />

<!-- Everything below the fold: lazy by default, no attribute needed -->
<img ngSrc="/images/feature-1.jpg" width="800" height="600" alt="…" />

Rules of thumb:

  • One or two images per page, the ones actually visible on load. Marking ten images priority makes them compete, and none of them loads first.
  • Let the warning guide you. In development, Angular detects the LCP element and logs a warning if it's an NgOptimizedImage without priority. Check the console on each important page at desktop and mobile widths, since the LCP element can differ.
  • Server rendering makes it stronger. With SSR, priority also emits a <link rel="preload"> in the document head, so the browser starts the download before it has parsed the page.

Responsive images with sizes #

A 1600-pixel hero is wasted on a 390-pixel phone. NgOptimizedImage can generate a srcset so the browser downloads a size that fits, but it needs two things: an image loader that can produce resized versions (next section), and, for images that change width with the layout, a sizes attribute describing how wide the image renders:

<img
  ngSrc="products/headphones.jpg"
  width="1600" height="900"
  sizes="(max-width: 768px) 100vw, 50vw"
  alt="Wireless headphones"
  priority />

sizes tells the browser the image takes the full viewport width on small screens and half of it on larger ones; the browser combines that with the device's pixel density to choose a candidate from the srcset. Angular generates candidates from a default list of breakpoints (from 16 up to 3840 pixels), which you can change via the IMAGE_CONFIG provider. Without sizes, a fixed-size image gets simple 1x/2x density candidates.

Image loaders: let a CDN resize for you #

A loader is a function that turns (src, width) into a URL your image service understands. Angular ships loaders for popular image CDNs:

// app.config.ts
import { provideImgixLoader } from '@angular/common';

export const appConfig: ApplicationConfig = {
  providers: [provideImgixLoader('https://your-account.imgix.net/')],
};

Built-in options include provideImgixLoader, provideCloudinaryLoader, provideImageKitLoader, provideCloudflareLoader and provideNetlifyLoader. If your images come from your own service, provide a custom loader through the IMAGE_LOADER token:

import { IMAGE_LOADER, ImageLoaderConfig } from '@angular/common';

export const appConfig: ApplicationConfig = {
  providers: [{
    provide: IMAGE_LOADER,
    useValue: (config: ImageLoaderConfig) =>
      `https://img.example.com/${config.src}?w=${config.width ?? 1200}&format=auto`,
  }],
};

With a loader configured, ngSrc becomes a path relative to the service (products/headphones.jpg), and the generated srcset asks for each width. That's where most of the byte savings come from: the right size in a modern format such as AVIF or WebP, without you exporting a dozen files.

fill mode for unknown dimensions #

Sometimes you don't know an image's size, such as user uploads, or you want it to cover a container. Use fill instead of width/height:

<div class="card-media">   <!-- position: relative; aspect-ratio: 16 / 9; -->
  <img ngSrc="uploads/cover.jpg" fill alt="Article cover" />
</div>

The image stretches to its parent, so the parent must be positioned (relative, absolute or fixed) and should have a size or aspect-ratio; otherwise the image collapses to nothing. Control cropping with CSS object-fit: cover. Use sizes with fill too, or the browser has to assume the image is full-width.

Placeholders #

While a large image loads, the directive can show a low-resolution blurred version. With a loader configured, add placeholder:

<img ngSrc="products/headphones.jpg" width="1600" height="900" alt="…" placeholder />

You can also pass a small base64 data URL as the placeholder value, and turn the blur off with [placeholderConfig]="{ blur: false }". Keep placeholders tiny: they're inlined into the HTML, so a big one slows down the page it's meant to speed up.

Migrating existing <img> tags #

A safe order for an existing app:

  1. Find the LCP image on your main pages with Lighthouse or DevTools' Performance panel, and convert it first with priority. This is usually the biggest single win.
  2. Convert above-the-fold images next, then the rest. Every conversion adds lazy loading and reserved space for free.
  3. Add real width/height. Angular warns in development when they don't match the file or distort the aspect ratio; fix each warning rather than silencing it.
  4. Add a loader once your images live on a CDN or image service, then add sizes to responsive images.
  5. Add preconnect for the image domain in index.html if the dev-mode warning asks for it.

What to leave alone: inline SVG icons, CSS background images, and tiny decorative images where the directive's requirements add effort without a measurable gain.

Gotchas #

Symptom Cause Fix
Console warning about the LCP image Hero image lacks priority Add priority to that one image
Layout jumps as images load Missing or wrong width/height Use the file's intrinsic size; let CSS scale it
Image squashed or stretched Attributes don't match the aspect ratio Fix the numbers (Angular warns in dev)
fill image invisible Parent not positioned or has no height position: relative plus a height or aspect-ratio
No srcset generated No loader configured Add a built-in or custom loader
Phones download the desktop image Responsive image without sizes Add a sizes attribute that matches the layout
Every image loads eagerly priority on too many images Keep it to the one or two visible on load

What's next #

Lesson 8.5 turns to the other big payload: your JavaScript. You'll analyse the production bundle, find what's making it large, and use lazy routes and @defer to ship less code up front. Lesson 8.6 then ties images, scripts and rendering together into a Core Web Vitals workflow for single-page apps.

Try it yourself #

Pick your app's most important page and run Lighthouse on it. Note the LCP element and time. Convert that image to ngSrc with correct dimensions and priority, convert the other images on the page, and run Lighthouse again. Then open the console in development mode at mobile width and fix any warnings the directive reports.

YouLighthouse says our product page LCP is 4.1 seconds and the LCP element is the main product photo. We already use NgOptimizedImage. What are we missing?
Claude · used search_documentationThree things in product-gallery.html. (1) The main photo has no priority, so it’s lazy-loaded like every other image; add priority to that one image. (2) All six thumbnails below it have priority too, which makes them compete with the main photo; remove it from them. (3) The main photo is responsive but has no sizes, so phones download the 2000-pixel version; the layout shows it at full width on mobile and about 60% on desktop, so use sizes="(max-width: 768px) 100vw, 60vw". Your Cloudinary loader is already configured, so the srcset will start working as soon as sizes is there. Re-run Lighthouse at mobile width after the change.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *