Skip to content

Commit 0c8eb8b

Browse files
AndrewKushnirPawel Kozlowski
authored andcommitted
perf(common): monitor LCP only for images without priority attribute (#47082)
This commit optimizes the logic that monitors whether a give image is an LCP element. If an image has the `priority` attribute set, there is no need to include it into monitoring. Also, if we already warned about a particular image (via a `console.warn`) - there is no need to warn again later (to avoid spamming a console). PR Close #47082
1 parent 37e3a60 commit 0c8eb8b

File tree

1 file changed

+38
-27
lines changed

1 file changed

+38
-27
lines changed

packages/common/src/directives/ng_optimized_image.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ export const IMAGE_LOADER = new InjectionToken<ImageLoader>('ImageLoader', {
6363
providedIn: 'root',
6464
})
6565
class 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

Comments
 (0)