diff --git a/goldens/public-api/common/errors.api.md b/goldens/public-api/common/errors.api.md
index 5ed18588252..ff719b033d1 100644
--- a/goldens/public-api/common/errors.api.md
+++ b/goldens/public-api/common/errors.api.md
@@ -37,6 +37,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
TOO_MANY_PRELOADED_IMAGES = 2961,
// (undocumented)
+ TOO_MANY_PRIORITY_ATTRIBUTES = 2966,
+ // (undocumented)
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
// (undocumented)
UNEXPECTED_INPUT_CHANGE = 2953,
diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
index dc4d53c850b..33d14674b7e 100644
--- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
+++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
@@ -137,6 +137,18 @@ export const BUILT_IN_LOADERS = [
netlifyLoaderInfo,
];
+/**
+ * Threshold for the PRIORITY_TRUE_COUNT
+ */
+const PRIORITY_COUNT_THRESHOLD = 10;
+
+/**
+ * This count is used to log a devMode warning
+ * when the count of directive instances with priority=true
+ * exceeds the threshold PRIORITY_COUNT_THRESHOLD
+ */
+let IMGS_WITH_PRIORITY_ATTR_COUNT = 0;
+
/**
* Config options used in rendering placeholder images.
*
@@ -430,6 +442,14 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
if (this.priority) {
const checker = this.injector.get(PreconnectLinkChecker);
checker.assertPreconnect(this.getRewrittenSrc(), this.ngSrc);
+
+ // This leaves the Angular zone to avoid triggering unnecessary change detection cycles when
+ // document.addEventListener is invoked
+ if (!this.isServer) {
+ ngZone.runOutsideAngular(() => {
+ assetPriorityCountBelowThreshold();
+ });
+ }
}
}
if (this.placeholder) {
@@ -1245,6 +1265,27 @@ function assertNoLoaderParamsWithoutLoader(dir: NgOptimizedImage, imageLoader: I
}
}
+/**
+ * Warns if the priority attribute is used too often on page load
+ */
+function assetPriorityCountBelowThreshold() {
+ if (IMGS_WITH_PRIORITY_ATTR_COUNT === 0) {
+ document.addEventListener('DOMContentLoaded', (event) => {
+ if (IMGS_WITH_PRIORITY_ATTR_COUNT > PRIORITY_COUNT_THRESHOLD) {
+ console.warn(
+ formatRuntimeError(
+ RuntimeErrorCode.TOO_MANY_PRIORITY_ATTRIBUTES,
+ `NgOptimizedImage: The "priority" attribute is set to true more than ${PRIORITY_COUNT_THRESHOLD} times (${IMGS_WITH_PRIORITY_ATTR_COUNT} times). ` +
+ `Marking too many images as "high" priority can hurt your application's LCP (https://web.dev/lcp).` +
+ `"Priority" should only be set on the image expected to be the page's LCP element.`,
+ ),
+ );
+ }
+ });
+ }
+ IMGS_WITH_PRIORITY_ATTR_COUNT++;
+}
+
function round(input: number): number | string {
return Number.isInteger(input) ? input : input.toFixed(2);
}
diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts
index 17af8225d9e..6970f1c2def 100644
--- a/packages/common/src/errors.ts
+++ b/packages/common/src/errors.ts
@@ -39,4 +39,5 @@ export const enum RuntimeErrorCode {
MISSING_NECESSARY_LOADER = 2963,
LCP_IMG_NGSRC_MODIFIED = 2964,
OVERSIZED_PLACEHOLDER = 2965,
+ TOO_MANY_PRIORITY_ATTRIBUTES = 2966,
}
diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts
index 04496b19e55..83304d0e57a 100644
--- a/packages/common/test/directives/ng_optimized_image_spec.ts
+++ b/packages/common/test/directives/ng_optimized_image_spec.ts
@@ -870,6 +870,51 @@ describe('Image directive', () => {
const img = nativeElement.querySelector('img')!;
expect(img.getAttribute('fetchpriority')).toBe('auto');
});
+
+ it('should log a warning if the priority attribute is used too often', async () => {
+ withHead('', () => {
+ const imageLoader = () => {
+ // We need something different from the `localhost` (as we don't want to produce
+ // a preconnect warning for local environments).
+ return 'https://angular.io/assets/images/logos/angular/angular.svg';
+ };
+
+ setupTestingModule({imageLoader});
+
+ // 11 priority attributes, threshold is 10
+ const template = `
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const consoleWarnSpy = spyOn(console, 'warn');
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+
+ // We manually fire the event that is listened by the directive
+ // as it won't fire in the context of our unit test
+ document.dispatchEvent(new Event('DOMContentLoaded'));
+
+ if (isBrowser) {
+ expect(consoleWarnSpy.calls.count()).toBe(1);
+ expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch(
+ new RegExp(`NG0${RuntimeErrorCode.TOO_MANY_PRIORITY_ATTRIBUTES}`),
+ );
+ } else {
+ // The warning is only logged on browsers
+ expect(consoleWarnSpy.calls.count()).toBe(0);
+ }
+ });
+ });
});
describe('meta data', () => {