@@ -63,8 +63,10 @@ export const IMAGE_LOADER = new InjectionToken<ImageLoader>('ImageLoader', {
6363 providedIn : 'root' ,
6464} )
6565class LCPImageObserver implements OnDestroy {
66- // Map of full image URLs -> image metadata (`raw-src` and `priority`).
67- private images = new Map < string , { rawSrc : string , priority : boolean } > ( ) ;
66+ // Map of full image URLs -> original `rawSrc` values.
67+ private images = new Map < string , string > ( ) ;
68+ // Keep track of images for which `console.warn` was produced.
69+ private alreadyWarned = new Set < string > ( ) ;
6870
6971 private window : Window | null = null ;
7072 private observer : PerformanceObserver | null = null ;
@@ -87,33 +89,36 @@ class LCPImageObserver implements OnDestroy {
8789 private initPerformanceObserver ( ) : PerformanceObserver {
8890 const observer = new PerformanceObserver ( ( entryList ) => {
8991 const entries = entryList . getEntries ( ) ;
90- if ( entries . length > 0 ) {
91- // Note: we use the latest entry produced by the `PerformanceObserver` as the best
92- // signal on which element is actually an LCP one. As an example, the first image to load on
93- // a page, by virtue of being the only thing on the page so far, is often a LCP candidate
94- // and gets reported by PerformanceObserver, but isn't necessarily the LCP element.
95- const lcpElement = entries [ entries . length - 1 ] ;
96- // Cast to `any` due to missing `element` on observed type of entry.
97- const imgSrc = ( lcpElement as any ) . element ?. src ?? '' ;
98- const img = this . images . get ( imgSrc ) ;
99- // Exclude `data:` and `blob:` URLs, since they are not supported by the directive.
100- if ( img && ! img . priority && ! imgSrc . startsWith ( 'data:' ) && ! imgSrc . startsWith ( 'blob:' ) ) {
101- const directiveDetails = imgDirectiveDetails ( { rawSrc : img . rawSrc } as any ) ;
102- console . warn ( formatRuntimeError (
103- RuntimeErrorCode . LCP_IMG_MISSING_PRIORITY ,
104- `${ directiveDetails } : the image was detected as the Largest Contentful Paint (LCP) ` +
105- `element, so its loading should be prioritized for optimal performance. Please ` +
106- `add the "priority" attribute if this image is above the fold.` ) ) ;
107- }
92+ if ( entries . length === 0 ) return ;
93+ // Note: we use the latest entry produced by the `PerformanceObserver` as the best
94+ // signal on which element is actually an LCP one. As an example, the first image to load on
95+ // a page, by virtue of being the only thing on the page so far, is often a LCP candidate
96+ // and gets reported by PerformanceObserver, but isn't necessarily the LCP element.
97+ const lcpElement = entries [ entries . length - 1 ] ;
98+ // Cast to `any` due to missing `element` on observed type of entry.
99+ const imgSrc = ( lcpElement as any ) . element ?. src ?? '' ;
100+
101+ // Exclude `data:` and `blob:` URLs, since they are not supported by the directive.
102+ if ( imgSrc . startsWith ( 'data:' ) || imgSrc . startsWith ( 'blob:' ) ) return ;
103+
104+ const imgRawSrc = this . images . get ( imgSrc ) ;
105+ if ( imgRawSrc && ! this . alreadyWarned . has ( imgSrc ) ) {
106+ this . alreadyWarned . add ( imgSrc ) ;
107+ const directiveDetails = imgDirectiveDetails ( { rawSrc : imgRawSrc } as any ) ;
108+ console . warn ( formatRuntimeError (
109+ RuntimeErrorCode . LCP_IMG_MISSING_PRIORITY ,
110+ `${ directiveDetails } : the image was detected as the Largest Contentful Paint (LCP) ` +
111+ `element, so its loading should be prioritized for optimal performance. Please ` +
112+ `add the "priority" attribute if this image is above the fold.` ) ) ;
108113 }
109114 } ) ;
110115 observer . observe ( { type : 'largest-contentful-paint' , buffered : true } ) ;
111116 return observer ;
112117 }
113118
114- registerImage ( rewrittenSrc : string , rawSrc : string , priority : boolean ) {
119+ registerImage ( rewrittenSrc : string , rawSrc : string ) {
115120 if ( ! this . observer ) return ;
116- this . images . set ( this . getFullUrl ( rewrittenSrc ) , { rawSrc, priority } ) ;
121+ this . images . set ( this . getFullUrl ( rewrittenSrc ) , rawSrc ) ;
117122 }
118123
119124 unregisterImage ( rewrittenSrc : string ) {
@@ -125,6 +130,7 @@ class LCPImageObserver implements OnDestroy {
125130 if ( ! this . observer ) return ;
126131 this . observer . disconnect ( ) ;
127132 this . images . clear ( ) ;
133+ this . alreadyWarned . clear ( ) ;
128134 }
129135}
130136
@@ -208,10 +214,15 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
208214 assertNotBlobURL ( this ) ;
209215 assertRequiredNumberInput ( this , this . width , 'width' ) ;
210216 assertRequiredNumberInput ( this , this . height , 'height' ) ;
211- withLCPImageObserver (
212- this . injector ,
213- ( observer : LCPImageObserver ) =>
214- observer . registerImage ( this . getRewrittenSrc ( ) , this . rawSrc , this . priority ) ) ;
217+ if ( ! this . priority ) {
218+ // Monitor whether an image is an LCP element only in case
219+ // the `priority` attribute is missing. Otherwise, an image
220+ // has the necessary settings and no extra checks are required.
221+ withLCPImageObserver (
222+ this . injector ,
223+ ( observer : LCPImageObserver ) =>
224+ observer . registerImage ( this . getRewrittenSrc ( ) , this . rawSrc ) ) ;
225+ }
215226 }
216227 this . setHostAttribute ( 'loading' , this . getLoadingBehavior ( ) ) ;
217228 this . setHostAttribute ( 'fetchpriority' , this . getFetchPriority ( ) ) ;
@@ -246,7 +257,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
246257 }
247258
248259 ngOnDestroy ( ) {
249- if ( ngDevMode ) {
260+ if ( ngDevMode && ! this . priority ) {
250261 // An image is only registered in dev mode, try to unregister only in dev mode as well.
251262 withLCPImageObserver (
252263 this . injector ,
0 commit comments