Image Display — Responsive Images, Lazy Loading and CDN Optimisation

Displaying images correctly is as important as uploading them. Responsive images served at appropriate sizes, lazy loading for off-screen images, and proper layout dimensions prevent layout shifts and reduce bandwidth. A CDN in front of Azure Blob Storage adds global edge caching — users in Asia receive images from an edge server near them rather than from a data center in the US, reducing latency from 300ms to 30ms.

Responsive Images and CDN Integration

// ── Angular image URL pipe — transform blob URL to CDN URL ─────────────────
@Pipe({ name: 'cdnUrl', standalone: true, pure: true })
export class CdnUrlPipe implements PipeTransform {
  private config = inject(APP_CONFIG);

  transform(url: string | null | undefined, size?: 'thumb' | 'medium' | 'large'): string {
    if (!url) return '/assets/images/placeholder.webp';

    // Transform Azure Blob URL to CDN URL
    // From: https://blogappstorage.blob.core.windows.net/images/abc.webp
    // To:   https://cdn.blogapp.com/images/abc.webp
    if (url.includes('blob.core.windows.net') && this.config.cdnBaseUrl) {
      const path = url.split('.net').pop();  // /images/abc.webp
      return `${this.config.cdnBaseUrl}${path}`;
    }

    return url;
  }
}

// ── Usage in templates ────────────────────────────────────────────────────
// <img [src]="post.coverImageUrl | cdnUrl" alt="Cover"
//      loading="lazy"
//      width="1200" height="630"
//      (error)="onImageError($event)">
//
// <img [src]="user.avatarUrl | cdnUrl" alt="Avatar"
//      loading="lazy"
//      width="200" height="200">
// ── PostCardComponent — optimised image display ────────────────────────────
@Component({
  selector:   'app-post-card',
  standalone:  true,
  imports:    [CdnUrlPipe, RouterLink, DatePipe],
  template: `
    <article class="post-card">
      <!-- Cover image with responsive sizing and lazy loading ─────────────── -->
      @if (post.coverImageUrl) {
        <a [routerLink]="['/posts', post.slug]" class="cover-link">
          <img
            [src]="post.coverImageUrl | cdnUrl"
            [alt]="post.title + ' cover image'"
            loading="lazy"          <!-- native lazy loading ──────────────── -->
            width="800"             <!-- prevents layout shift (CLS) ────────── -->
            height="450"
            class="cover-image"
            (error)="onImageError($event)">
        </a>
      }

      <div class="card-content">
        <!-- Author avatar ───────────────────────────────────────────────── -->
        <div class="author-row">
          <img
            [src]="post.authorAvatarUrl | cdnUrl"
            [alt]="post.authorName + ' avatar'"
            loading="lazy"
            width="32" height="32"
            class="avatar"
            (error)="onAvatarError($event)">
          <span>{{ post.authorName }}</span>
          <time [dateTime]="post.publishedAt">
            {{ post.publishedAt | date:'mediumDate' }}
          </time>
        </div>

        <h2><a [routerLink]="['/posts', post.slug]">{{ post.title }}</a></h2>
        <p>{{ post.excerpt }}</p>

        <div class="meta">
          <span>👁 {{ post.viewCount | number }}</span>
          <span>💬 {{ post.commentCount }}</span>
        </div>
      </div>
    </article>
  `,
})
export class PostCardComponent {
  @Input({ required: true }) post!: PostSummaryDto;

  onImageError(event: Event): void {
    (event.target as HTMLImageElement).src = '/assets/images/post-placeholder.webp';
  }

  onAvatarError(event: Event): void {
    (event.target as HTMLImageElement).src = '/assets/images/avatar-default.webp';
  }
}
Note: The width and height attributes on <img> elements are critical for preventing Cumulative Layout Shift (CLS) — a Core Web Vitals metric. When the browser knows the image dimensions before it downloads the image, it reserves the correct space in the layout. Without these attributes, the page reflows (shifts layout) when the image loads, which degrades user experience and hurts search engine rankings. Set width and height to the image’s natural dimensions (or the dimensions it will be displayed at).
Tip: Implement Azure CDN or Cloudflare in front of Azure Blob Storage with a single configuration change — point the CDN origin to your blob storage account URL. Once configured, swap all blob URLs to CDN URLs using the pipe. Azure CDN with Microsoft’s network caches at 100+ edge locations globally; images that were 300ms away are now 20-50ms away for users worldwide. The CDN also reduces your blob storage egress costs (CDN to end users is cheaper than blob storage to end users).
Warning: The loading="lazy" attribute defers loading images until they are near the viewport. Do NOT add it to images that are visible on page load (above the fold) — particularly the largest image on the page (the LCP — Largest Contentful Paint). Lazy loading the LCP image delays it from starting to load, which hurts the LCP score (another Core Web Vitals metric). Use loading="eager" (or omit the attribute) for the first visible image, and loading="lazy" for everything below the fold.

Common Mistakes

Mistake 1 — No width/height on images (layout shift on load)

❌ Wrong — <img [src]="..."> without width/height; page reflows when image loads; poor CLS score.

✅ Correct — always specify width and height matching the displayed dimensions; browser reserves space before download.

Mistake 2 — loading=”lazy” on above-the-fold LCP image (hurts Core Web Vitals)

❌ Wrong — hero image with loading=”lazy”; browser defers it; LCP score degrades significantly.

✅ Correct — first visible image uses loading="eager" or omits the attribute; only below-fold images get loading="lazy".

🧠 Test Yourself

The CdnUrlPipe receives a null coverImageUrl from a post that has no cover image. What URL does it return?