Angular NgOptimizedImage and BlurHash

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. NgOptimizedImage is the image directive of @angular/common. It applies the practices of native HTML images automatically: lazy loading by default, fetchpriority and preloading for the priority image, a generated srcset, and development-time warnings for missing dimensions or oversized images. The official guide is the Angular image optimization guide. The framework itself is covered in the Angular section. Check the guide for the Angular version you use, since options have been added in several minor releases.

Basic usage

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

@Component({
  selector: 'app-cover',
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="b81f/cover.jpg" width="640" height="960" sizes="(min-width: 60em) 20vw, 50vw" alt="Cover of Moby-Dick">
  `,
})
export class CoverComponent {}
  • Use ngSrc instead of src. Do not set src as well.

  • width and height are required unless you use fill, because they reserve the layout space.

  • The directive adds loading="lazy" and decoding="async" by default. For the LCP image, add the priority attribute, which sets fetchpriority="high", turns lazy loading off and (with a loader) adds a preload link in the head during server-side rendering.

  • fill makes the image fill its parent, like object-fit and position: absolute. The parent needs position: relative and a size. Use it with sizes.

  • sizes plus a loader produces a width-based srcset. A fixed-size image gets density descriptors (1x, 2x) instead. With ngSrcset you list your own widths.

<div style="position: relative; aspect-ratio: 2 / 3">
  <img ngSrc="b81f/cover.jpg" fill sizes="(min-width: 60em) 20vw, 50vw" alt="Cover of Moby-Dick">
</div>
<img ngSrc="hero/library.jpg" width="1920" height="1080" priority sizes="100vw" alt="Reading room">

Loaders

A loader turns ngSrc and a width into a URL. Without a loader the directive still adds lazy loading and the layout attributes but does not generate a srcset. Angular ships loaders for several image CDNs, each provided in the application configuration with the base URL of the service:

import { ApplicationConfig } from '@angular/core';
import {
  provideCloudflareLoader, provideCloudinaryLoader, provideImageKitLoader, provideImgixLoader, provideNetlifyLoader,
} from '@angular/common';

export const appConfig: ApplicationConfig = {
  providers: [
    provideImgixLoader('https://example.imgix.net'),
    // provideCloudinaryLoader('https://res.cloudinary.com/<cloud>'),
    // provideImageKitLoader('https://ik.imagekit.io/<id>'),
    // provideCloudflareLoader('https://example.com'),
    // provideNetlifyLoader(),
  ],
};

The NgOptimizedImage API reference lists the loader functions and their arguments. Image CDN documentation: Cloudinary, imgix.

A custom loader for your own variants

For the variants produced by the backend (see responsive images and thumbnails), provide your own IMAGE_LOADER:

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

const WIDTHS = [320, 640, 1280];

export const mediaLoaderProvider = {
  provide: IMAGE_LOADER,
  useValue: (config: ImageLoaderConfig) => {
    const requested = config.width ?? WIDTHS[0];
    const width = WIDTHS.find((w) => w >= requested) ?? WIDTHS[WIDTHS.length - 1];
    return `https://cdn.example.com/covers/${config.src}/w${width}.avif`;
  },
};

Here ngSrc carries the image id, and the loader chooses the smallest variant that is at least as wide as the request. Add the provider to appConfig.providers. The loader decides the format, so the AVIF-with-fallback decision is made by the backend or CDN (the Accept header, see image formats).

Placeholders

NgOptimizedImage has a placeholder attribute. Set it to true to ask the loader’s CDN for a tiny version of the image, or give it a data URL. It blurs the placeholder by default, which can be changed with placeholderConfig:

<img ngSrc="b81f/cover.jpg" width="640" height="960" [placeholder]="placeholderUrl" [placeholderConfig]="{ blur: true }" alt="">

When the placeholder is a data URL, Angular recommends keeping it small, about 4 KB or less, because it is inlined in the HTML and the component.

BlurHash with the blurhash package

The Angular wrappers for BlurHash (ng-blurhash, ngx-blurhash, angular-blurhash) are not maintained, so decode directly with the blurhash package and pass the result as the placeholder.

npm install blurhash
import { Injectable } from '@angular/core';
import { decode } from 'blurhash';

@Injectable({ providedIn: 'root' })
export class BlurhashService {
  toDataUrl(hash: string, width = 16, height = 16): string {
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext('2d')!;
    context.putImageData(new ImageData(decode(hash, width, height), width, height), 0, 0);
    return canvas.toDataURL();
  }
}
import { Component, computed, inject, input } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-book-cover',
  imports: [NgOptimizedImage],
  template: `<img [ngSrc]="cover().id" [width]="cover().width" [height]="cover().height"
                  [placeholder]="placeholder()" sizes="50vw" [alt]="alt()">`,
})
export class BookCoverComponent {
  private readonly blurhash = inject(BlurhashService);
  cover = input.required<{ id: string; width: number; height: number; blurhash: string }>();
  alt = input('');
  // computed() decodes once per hash, not on every change detection
  placeholder = computed(() => this.blurhash.toDataUrl(this.cover().blurhash));
}

document and canvas do not exist during server-side rendering, so guard the service with isPlatformBrowser, or decode on the server with sharp as in the Next.js page. The computed() signal above decodes once per hash; a getter would decode again on every change detection. The BlurHash algorithm is in the algorithm page.