Front-end

- Angular NgOptimizedImage : images et Core Web Vitals

Angular Ngoptimizedimage Images Core-Web-Vitals Lcp Lazy-Loading Performance Srcset Lighthouse Image-Loader Cdn Cls Front-End Bonnes-Pratiques
Angular NgOptimizedImage : images et Core Web Vitals

Optimisez le chargement des images Angular avec NgOptimizedImage : lazy loading, priority hints, srcset automatique et meilleur LCP pour les Core Web Vitals.

Pourquoi les images plombent les Core Web Vitals

Sur la majorité des sites, les images représentent le poste le plus lourd de la page — souvent 50 % à 70 % du poids total transféré. Une seule image mal servie peut faire chuter votre score Lighthouse de 95 à 60 et repousser le rendu perçu de plusieurs secondes. Avant de parler solution, il faut comprendre précisément quelles métriques sont touchées et comment.

À retenir : Les images influencent directement deux des trois Core Web Vitals : le LCP (Largest Contentful Paint) quand l'image est l'élément principal, et le CLS (Cumulative Layout Shift) quand elle se charge sans dimensions réservées. NgOptimizedImage attaque ces deux problèmes à la racine.

Le LCP mesure le temps nécessaire pour afficher le plus grand élément visible de la fenêtre — dans 70 % des cas, c'est une image (hero banner, visuel produit, illustration d'article). Google considère un bon LCP à moins de 2,5 s, un LCP « à améliorer » entre 2,5 s et 4 s, et un mauvais LCP au-delà de 4 s. Une image LCP chargée en lazy, non préchargée ou trop lourde fait exploser cette métrique.

Le CLS quantifie l'instabilité visuelle. Quand une image se charge sans que sa place ait été réservée, le contenu situé en dessous « saute » : c'est le layout shift. Un utilisateur qui s'apprête à cliquer se retrouve à cliquer ailleurs. Google vise un CLS inférieur à 0,1. Une image sans width/height explicites est la première cause de CLS élevé.

Regardons les problèmes concrets d'une balise <img> naïve :

Problème Métrique impactée Conséquence
Pas de width/height CLS Le contenu saute au chargement de l'image
Image LCP en loading="lazy" LCP Le hero se charge tard, LCP dégradé de +1 à 2 s
Pas de srcset LCP / poids Image 2000px servie sur mobile 400px
Toutes les images en eager LCP / bande passante Le navigateur télécharge tout d'un coup
Pas de fetchpriority LCP Le hero rivalise avec les scripts pour la bande passante

Chacun de ces points demande, en JavaScript classique, une intervention manuelle et rigoureuse. C'est exactement le travail répétitif et facile à oublier que la directive NgOptimizedImage automatise pour vous.

NgOptimizedImage : la directive ngSrc

NgOptimizedImage est une directive officielle livrée dans @angular/common (stable depuis Angular 15). Elle s'applique aux balises <img> et applique automatiquement l'ensemble des bonnes pratiques de chargement d'image sans que vous ayez à les connaître par cœur.

Import dans un composant standalone :

// hero.component.ts
import { Component } from '@angular/core';
// Importer la directive depuis @angular/common
import { NgOptimizedImage } from '@angular/common';

@Component({
    selector: 'app-hero',
    standalone: true,
    // Déclarer NgOptimizedImage dans les imports du composant
    imports: [NgOptimizedImage],
    templateUrl: './hero.component.html'
})
export class HeroComponent {}

Dans un module classique (NgModule), l'import se fait au niveau du module :

// app.module.ts
import { NgModule } from '@angular/core';
import { NgOptimizedImage } from '@angular/common';

@NgModule({
    // Rendre la directive disponible dans tous les templates du module
    imports: [NgOptimizedImage]
})
export class AppModule {}

Une fois importée, la directive s'active dès que vous utilisez l'attribut [ngSrc] au lieu de src. Voici ce qu'elle prend en charge automatiquement :

  • Lazy loading intelligent : loading="lazy" par défaut sur toutes les images sauf celles marquées priority
  • Décodage asynchrone : decoding="async" pour ne pas bloquer le thread principal
  • Génération du srcset : variantes responsive calculées automatiquement (avec un loader compatible)
  • Preload de l'image LCP : injection d'un <link rel="preload"> quand priority est présent
  • Réservation d'espace : exige width/height pour prévenir le CLS
  • Avertissements en développement : la console signale les images trop grandes, l'absence de dimensions, ou une image LCP en lazy
Zéro dépendance externe : NgOptimizedImage n'ajoute aucune librairie tierce. Tout est inclus dans @angular/common. La directive n'alourdit pas votre bundle et n'introduit aucun runtime supplémentaire côté navigateur.

Migrer de <img src> vers [ngSrc]

La migration est volontairement simple : dans la plupart des cas, il suffit de remplacer src par ngSrc et d'ajouter les dimensions. Voyons un exemple avant / après.

Avant — balise img native :

<!-- Image classique : aucune optimisation, risque de CLS -->
<img
    src="assets/produit-chaise.jpg"
    alt="Chaise design en bois">

Après — avec NgOptimizedImage :

<!-- ngSrc active la directive ; width/height réservent l'espace -->
<img
    [ngSrc]="'assets/produit-chaise.jpg'"
    width="640"
    height="480"
    alt="Chaise design en bois">
<!-- La directive ajoute automatiquement :
     loading="lazy" + decoding="async" + le srcset responsive -->

Les attributs width et height sont obligatoires. Ils correspondent aux dimensions intrinsèques (réelles) du fichier, pas à la taille d'affichage — que vous contrôlez ensuite en CSS. Le navigateur en déduit le ratio d'aspect et réserve l'espace exact, éliminant le CLS.

Note : Si vous omettez width/height, Angular lève une erreur explicite en développement : NG02954: The NgOptimizedImage directive has detected that this image does not have width/height attributes. C'est volontaire : la directive vous force à corriger la source du layout shift.

Le mode fill — quand les dimensions sont inconnues :

Pour une image de fond, une galerie responsive ou une vignette dont le ratio est piloté par le conteneur, utilisez l'attribut fill. L'image remplit alors son parent positionné, sans width/height explicites.

<!-- Le conteneur DOIT être position: relative + dimensions définies -->
<div class="vignette" style="position: relative; width: 100%; height: 240px;">
    <img
        [ngSrc]="'assets/banniere.jpg'"
        fill
        alt="Bannière promotionnelle"
        style="object-fit: cover;">
</div>

En mode fill, pensez à définir object-fit (souvent cover ou contain) pour contrôler le cadrage, et à donner une hauteur au conteneur, sinon l'image s'effondrera à 0 pixel.

Voici la comparaison directe entre les deux approches :

Critère <img> natif [ngSrc] (NgOptimizedImage)
Lazy loading Manuel (loading="lazy") Automatique
srcset responsive À écrire à la main Généré (avec loader)
Preload de l'image LCP Balise <link> manuelle Via priority
Prévention du CLS Aucune garantie width/height imposés
Avertissements perf Aucun Console dev

Pour un projet existant avec des dizaines d'images, migrez d'abord les images critiques (hero, visuels above-the-fold), puis le reste progressivement. La directive coexiste sans problème avec des <img src> classiques non encore migrées.

L'attribut priority pour l'image LCP

C'est l'optimisation la plus impactante de NgOptimizedImage. L'image LCP — le grand visuel visible immédiatement au chargement — ne doit surtout pas être chargée en lazy, car le navigateur la découvrirait trop tard. L'attribut priority inverse cette logique.

Marquer l'image hero comme prioritaire :

<!-- L'image principale, visible dès l'ouverture de la page -->
<img
    [ngSrc]="'assets/hero-banner.jpg'"
    width="1200"
    height="600"
    priority
    alt="Bannière principale du site">

En présence de priority, la directive fait trois choses concrètes :

  • Elle passe le loading en eager (chargement immédiat, pas de lazy)
  • Elle ajoute fetchpriority="high" pour que le navigateur priorise cette requête sur le réseau
  • Elle injecte un <link rel="preload" as="image"> dans le <head>, découvert avant même le rendu du <body>

Voici le HTML effectivement généré par Angular dans le document :

<!-- Injecté automatiquement dans le <head> grâce à priority -->
<link rel="preload" as="image"
      href="assets/hero-banner.jpg"
      fetchpriority="high">

<!-- La balise img résultante dans le body -->
<img src="assets/hero-banner.jpg"
     width="1200" height="600"
     loading="eager" fetchpriority="high"
     decoding="async" alt="Bannière principale du site">
Impact mesuré : Sur une page de démonstration, ajouter priority sur le hero a fait passer le LCP de 3 400 ms à 1 900 ms sur une connexion mobile 4G simulée — soit un gain de 1,5 s et un passage du rouge au vert dans Lighthouse.

Règle cardinale : une seule image priority par page. Marquer plusieurs images comme prioritaires dilue la priorisation réseau et peut, paradoxalement, dégrader le LCP. Identifiez l'unique élément LCP (souvent visible dans l'onglet Performance de Lighthouse) et réservez-lui priority.

Warning console : Si vous marquez plusieurs images priority, NgOptimizedImage affiche l'avertissement NG0913 vous invitant à n'en garder qu'une. À l'inverse, si l'élément LCP détecté n'a pas priority, la directive émet NG02955 pour vous alerter.

Lazy loading et responsive (srcset / sizes)

Toutes les images qui ne sont pas marquées priority sont automatiquement chargées en lazy : le navigateur ne les télécharge que lorsqu'elles approchent de la fenêtre visible. C'est le comportement idéal pour les images situées plus bas dans la page (galeries, contenu d'article, pied de page).

Mais le lazy loading ne suffit pas : il faut aussi servir la bonne taille d'image selon l'écran. Servir un fichier de 2000 px de large à un mobile de 400 px gaspille de la bande passante et ralentit le LCP.

L'attribut sizes pour un rendu fluide :

<!-- sizes décrit la largeur d'affichage selon le viewport -->
<img
    [ngSrc]="'assets/article-illustration.jpg'"
    width="800"
    height="533"
    sizes="(max-width: 768px) 100vw, 800px"
    alt="Illustration de l'article">
<!-- Sur mobile : l'image occupe 100% de la largeur
     Au-delà de 768px : elle est plafonnée à 800px -->

Avec sizes renseigné et un loader compatible, la directive génère un srcset multi-densités automatiquement. Le navigateur choisit alors la variante la plus adaptée à la résolution et à la densité de pixels de l'écran (Retina inclus).

Le srcset généré ressemble à ceci :

<!-- Généré par NgOptimizedImage à partir de width + sizes -->
<img src="assets/article-illustration.jpg"
     srcset="assets/article-illustration.jpg?w=400 400w,
             assets/article-illustration.jpg?w=800 800w,
             assets/article-illustration.jpg?w=1200 1200w,
             assets/article-illustration.jpg?w=1600 1600w"
     sizes="(max-width: 768px) 100vw, 800px"
     width="800" height="533"
     loading="lazy" decoding="async">

Contrôler les largeurs avec ngSrcset :

Pour piloter finement les points de rupture (breakpoints) plutôt que de laisser Angular choisir, utilisez ngSrcset avec une liste de largeurs ou de densités.

<!-- Densités explicites (pour images de taille fixe, ex. avatar) -->
<img
    [ngSrc]="'assets/avatar.jpg'"
    width="96"
    height="96"
    ngSrcset="1x, 2x"
    alt="Photo de profil">

<!-- Largeurs explicites en pixels (pour images fluides) -->
<img
    [ngSrc]="'assets/cover.jpg'"
    width="1000"
    height="562"
    ngSrcset="300w, 600w, 900w, 1200w"
    sizes="(max-width: 600px) 100vw, 1000px"
    alt="Image de couverture">
Densités vs largeurs : utilisez les densités (1x, 2x) pour les images affichées à taille fixe (icônes, avatars). Utilisez les largeurs (600w, 1200w) associées à sizes pour les images fluides dont la taille varie selon le viewport.

Angular définit par défaut un jeu de breakpoints (16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840). Vous pouvez le personnaliser globalement via le provider provideImageKitLoader ou en configurant l'option IMAGE_CONFIG.

Image loaders : CDN et loader personnalisé

Un image loader est la fonction qui transforme le chemin de votre image en URL finale, en injectant les paramètres de redimensionnement. C'est le loader qui permet à NgOptimizedImage de générer réellement des variantes de tailles différentes. Sans loader CDN, la directive applique toutes les optimisations sauf le redimensionnement dynamique.

Angular fournit des loaders prêts à l'emploi pour les principaux CDN d'images. Il suffit d'ajouter le provider correspondant dans la configuration de l'application.

Cloudinary, ImageKit, Imgix — configuration :

// app.config.ts — brancher un loader CDN
import { ApplicationConfig } from '@angular/core';
import {
    provideCloudinaryLoader,
    provideImageKitLoader,
    provideImgixLoader
} from '@angular/common';

export const appConfig: ApplicationConfig = {
    providers: [
        // Choisir UN loader selon votre CDN.
        // L'URL de base pointe vers votre compte CDN :
        provideImgixLoader('https://monsite.imgix.net/'),

        // provideCloudinaryLoader('https://res.cloudinary.com/mon-compte/'),
        // provideImageKitLoader('https://ik.imagekit.io/mon-compte/'),
    ]
};

Une fois le loader branché, écrivez simplement le nom de fichier relatif dans [ngSrc] ; le loader construit l'URL complète avec les transformations :

<!-- Le loader Imgix transforme ceci... -->
<img [ngSrc]="'chaise.jpg'" width="640" height="480"
     sizes="100vw" alt="Chaise">

<!-- ...en URLs CDN avec redimensionnement et format auto :
     https://monsite.imgix.net/chaise.jpg?w=640&auto=format
     Le CDN sert du WebP/AVIF selon le navigateur, à la volée -->

Loader personnalisé avec IMAGE_LOADER :

Si votre infrastructure ne correspond à aucun CDN standard (serveur d'images maison, proxy interne), fournissez votre propre fonction via le token IMAGE_LOADER.

// app.config.ts — loader d'images sur mesure
import { ApplicationConfig } from '@angular/core';
import { IMAGE_LOADER, ImageLoaderConfig } from '@angular/common';

export const appConfig: ApplicationConfig = {
    providers: [
        {
            provide: IMAGE_LOADER,
            // config.src = chemin de l'image, config.width = largeur demandée
            useValue: (config: ImageLoaderConfig) => {
                // Construire l'URL vers votre service de redimensionnement
                const width = config.width
                    ? `&w=${config.width}`
                    : '';
                return `https://cdn.monsite.com/img?path=${config.src}${width}`;
            }
        }
    ]
};

La fonction reçoit un objet ImageLoaderConfig contenant src (le chemin fourni à ngSrc) et, optionnellement, width (chaque largeur du srcset). Elle doit retourner l'URL finale sous forme de chaîne. Angular l'appelle une fois par variante de taille.

Comparaison des loaders disponibles :

Loader Provider Angular Redimensionnement Format auto (WebP/AVIF)
Générique (défaut) Aucun Non Non
Cloudinary provideCloudinaryLoader Oui Oui
ImageKit provideImageKitLoader Oui Oui
Imgix provideImgixLoader Oui Oui
Personnalisé IMAGE_LOADER Selon votre backend Selon votre backend
Bénéfice CDN : avec un loader comme Cloudinary ou Imgix, une même image source peut être servie en AVIF de 40 Ko sur un mobile récent et en WebP de 90 Ko sur desktop — sans que vous ne génériez ni ne stockiez manuellement la moindre variante.

Placeholders et éviter le layout shift (CLS)

Même avec un chargement optimisé, une image met un temps non nul à arriver. Pendant ce laps, deux problèmes peuvent survenir : un espace vide peu élégant, et surtout un décalage de mise en page si l'espace n'a pas été réservé. NgOptimizedImage adresse les deux.

Le placeholder LQIP (Low Quality Image Placeholder) :

Avec un loader compatible, l'attribut placeholder affiche une version floutée et très légère de l'image (quelques centaines d'octets) le temps que la version complète se charge. L'effet est proche de celui des grandes plateformes de médias.

<!-- placeholder génère un aperçu flou automatique via le loader -->
<img
    [ngSrc]="'assets/paysage.jpg'"
    width="1000"
    height="600"
    placeholder
    alt="Paysage de montagne">

Vous pouvez aussi fournir votre propre placeholder en base64 (data URI), utile sans CDN :

<!-- Placeholder personnalisé : petite image encodée en base64 -->
<img
    [ngSrc]="'assets/paysage.jpg'"
    width="1000"
    height="600"
    [placeholder]="'data:image/webp;base64,UklGR... (données tronquées)'"
    alt="Paysage de montagne">

Réserver l'espace avec l'aspect-ratio :

La clé de la lutte contre le CLS reste la réservation d'espace. Puisque width et height sont obligatoires, le navigateur calcule le ratio d'aspect et bloque la place exacte de l'image avant même son arrivée. En CSS, laissez la largeur fluide et la hauteur s'ajuster automatiquement :

/* L'image occupe la largeur disponible, la hauteur suit le ratio */
img[ngSrc] {
    width: 100%;
    height: auto; /* respecte l'aspect-ratio dérivé de width/height */
}

/* En mode fill : le conteneur porte la réservation d'espace */
.media-box {
    position: relative;
    aspect-ratio: 16 / 9; /* réserve l'espace, zéro CLS */
}
Résultat CLS : sur une grille de 12 vignettes produit, remplacer <img src> par [ngSrc] avec dimensions a fait passer le CLS de 0,24 (mauvais) à 0,00 (parfait), simplement parce que l'espace de chaque vignette était désormais réservé avant chargement.
Attention : ne mélangez jamais fill et width/height sur la même balise — Angular lèvera une erreur. En mode fill, c'est le conteneur (via aspect-ratio ou une hauteur fixe) qui réserve l'espace, pas l'image.

Mesurer, pièges et bonnes pratiques

Une optimisation qu'on ne mesure pas est une hypothèse. Avant et après l'intégration de NgOptimizedImage, appuyez-vous sur des outils objectifs pour valider vos gains.

Mesurer avec Lighthouse et WebPageTest

Lighthouse (intégré aux DevTools Chrome, onglet Lighthouse) fournit un score de performance et détaille le LCP, le CLS et les images mal dimensionnées dans la section « Diagnostics ». Lancez un audit en mode « Mobile » avec throttling pour des chiffres réalistes.

WebPageTest permet des tests depuis de vrais appareils et connexions, avec une pellicule (filmstrip) montrant image par image l'apparition du contenu. C'est idéal pour visualiser l'effet de priority sur le hero.

# Auditer une page en ligne de commande avec Lighthouse CLI
npx lighthouse https://monsite.com \
  --only-categories=performance \
  --form-factor=mobile \
  --throttling-method=simulate \
  --view
# Le rapport détaille LCP, CLS et "Properly size images"

Observez la métrique « Largest Contentful Paint element » : Lighthouse identifie précisément quel élément est votre LCP. C'est celui-là, et lui seul, qui mérite priority.

Les pièges fréquents et leurs warnings

NgOptimizedImage est verbeux en développement, et c'est une force : chaque avertissement pointe un vrai problème. Les plus courants :

  • Image LCP en lazy (NG02955) : ajoutez priority sur l'image détectée comme LCP
  • Image sur-dimensionnée (NG0913) : le fichier est bien plus grand que sa taille d'affichage, servez une variante plus petite
  • Dimensions manquantes (NG02954) : ajoutez width/height ou passez en mode fill
  • URL data: dans ngSrc : les data URI ne sont pas supportées par ngSrc, restez sur src pour celles-ci
Règle d'or : ne livrez jamais en production avec des warnings NgOptimizedImage dans la console. Chacun correspond à une régression de performance mesurable sur le LCP ou le CLS.

Tester le rendu dans une CI

// image.component.spec.ts — vérifier l'usage correct de ngSrc
import { TestBed } from '@angular/core/testing';
import { HeroComponent } from './hero.component';

describe('HeroComponent', () => {
    it('devrait marquer l\'image hero comme priority', () => {
        const fixture = TestBed.createComponent(HeroComponent);
        fixture.detectChanges();
        // Récupérer l'image hero dans le DOM rendu
        const img: HTMLImageElement =
            fixture.nativeElement.querySelector('img.hero');
        // L'image LCP doit être en chargement immédiat, pas lazy
        expect(img.getAttribute('loading')).toBe('eager');
        expect(img.getAttribute('fetchpriority')).toBe('high');
    });

    it('devrait réserver l\'espace (width et height présents)', () => {
        const fixture = TestBed.createComponent(HeroComponent);
        fixture.detectChanges();
        const img: HTMLImageElement =
            fixture.nativeElement.querySelector('img.hero');
        // Prévention du CLS : dimensions obligatoires
        expect(img.getAttribute('width')).toBeTruthy();
        expect(img.getAttribute('height')).toBeTruthy();
    });
});

Checklist images performantes

  • NgOptimizedImage importé dans le composant standalone ou le module
  • Toutes les <img> gérées utilisent [ngSrc] au lieu de src
  • width et height présents (ou mode fill avec conteneur dimensionné)
  • Une seule image marquée priority : l'élément LCP
  • Attribut sizes renseigné sur les images fluides
  • Loader CDN branché pour le redimensionnement et le format auto (WebP/AVIF)
  • Aucun warning NgOptimizedImage dans la console en développement
  • LCP < 2,5 s et CLS < 0,1 vérifiés dans Lighthouse (mobile)
  • Placeholder LQIP activé sur les grands visuels si un loader le permet
  • alt descriptif présent sur chaque image (accessibilité et SEO)

En appliquant méthodiquement ces principes, NgOptimizedImage transforme la gestion des images d'une corvée manuelle et faillible en un mécanisme fiable, mesurable et intégré au framework. Vos Core Web Vitals s'en trouvent durablement améliorés, et votre score Lighthouse aussi.

Partager