Skip to content

Commit cc4cb96

Browse files
JeanMechedylhunn
authored andcommitted
refactor(common): Log a warning when the priority attribute of NgOptimizedImage is used too often. (#56669)
When the DOM content is loaded, Angular will log a warning message if the `priority` attribute is applied to often on `NgOptimizedImage` directive instances. PR Close #56669
1 parent ec654f7 commit cc4cb96

File tree

4 files changed

+89
-0
lines changed

4 files changed

+89
-0
lines changed

goldens/public-api/common/errors.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const enum RuntimeErrorCode {
3737
// (undocumented)
3838
TOO_MANY_PRELOADED_IMAGES = 2961,
3939
// (undocumented)
40+
TOO_MANY_PRIORITY_ATTRIBUTES = 2966,
41+
// (undocumented)
4042
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
4143
// (undocumented)
4244
UNEXPECTED_INPUT_CHANGE = 2953,

packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ export const BUILT_IN_LOADERS = [
137137
netlifyLoaderInfo,
138138
];
139139

140+
/**
141+
* Threshold for the PRIORITY_TRUE_COUNT
142+
*/
143+
const PRIORITY_COUNT_THRESHOLD = 10;
144+
145+
/**
146+
* This count is used to log a devMode warning
147+
* when the count of directive instances with priority=true
148+
* exceeds the threshold PRIORITY_COUNT_THRESHOLD
149+
*/
150+
let IMGS_WITH_PRIORITY_ATTR_COUNT = 0;
151+
140152
/**
141153
* Config options used in rendering placeholder images.
142154
*
@@ -430,6 +442,14 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
430442
if (this.priority) {
431443
const checker = this.injector.get(PreconnectLinkChecker);
432444
checker.assertPreconnect(this.getRewrittenSrc(), this.ngSrc);
445+
446+
// This leaves the Angular zone to avoid triggering unnecessary change detection cycles when
447+
// document.addEventListener is invoked
448+
if (!this.isServer) {
449+
ngZone.runOutsideAngular(() => {
450+
assetPriorityCountBelowThreshold();
451+
});
452+
}
433453
}
434454
}
435455
if (this.placeholder) {
@@ -1245,6 +1265,27 @@ function assertNoLoaderParamsWithoutLoader(dir: NgOptimizedImage, imageLoader: I
12451265
}
12461266
}
12471267

1268+
/**
1269+
* Warns if the priority attribute is used too often on page load
1270+
*/
1271+
function assetPriorityCountBelowThreshold() {
1272+
if (IMGS_WITH_PRIORITY_ATTR_COUNT === 0) {
1273+
document.addEventListener('DOMContentLoaded', (event) => {
1274+
if (IMGS_WITH_PRIORITY_ATTR_COUNT > PRIORITY_COUNT_THRESHOLD) {
1275+
console.warn(
1276+
formatRuntimeError(
1277+
RuntimeErrorCode.TOO_MANY_PRIORITY_ATTRIBUTES,
1278+
`NgOptimizedImage: The "priority" attribute is set to true more than ${PRIORITY_COUNT_THRESHOLD} times (${IMGS_WITH_PRIORITY_ATTR_COUNT} times). ` +
1279+
`Marking too many images as "high" priority can hurt your application's LCP (https://web.dev/lcp).` +
1280+
`"Priority" should only be set on the image expected to be the page's LCP element.`,
1281+
),
1282+
);
1283+
}
1284+
});
1285+
}
1286+
IMGS_WITH_PRIORITY_ATTR_COUNT++;
1287+
}
1288+
12481289
function round(input: number): number | string {
12491290
return Number.isInteger(input) ? input : input.toFixed(2);
12501291
}

packages/common/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ export const enum RuntimeErrorCode {
3939
MISSING_NECESSARY_LOADER = 2963,
4040
LCP_IMG_NGSRC_MODIFIED = 2964,
4141
OVERSIZED_PLACEHOLDER = 2965,
42+
TOO_MANY_PRIORITY_ATTRIBUTES = 2966,
4243
}

packages/common/test/directives/ng_optimized_image_spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,51 @@ describe('Image directive', () => {
870870
const img = nativeElement.querySelector('img')!;
871871
expect(img.getAttribute('fetchpriority')).toBe('auto');
872872
});
873+
874+
it('should log a warning if the priority attribute is used too often', async () => {
875+
withHead('', () => {
876+
const imageLoader = () => {
877+
// We need something different from the `localhost` (as we don't want to produce
878+
// a preconnect warning for local environments).
879+
return 'https://angular.io/assets/images/logos/angular/angular.svg';
880+
};
881+
882+
setupTestingModule({imageLoader});
883+
884+
// 11 priority attributes, threshold is 10
885+
const template = `
886+
<img ngSrc="path/img.png" width="150" height="50" priority>
887+
<img ngSrc="path/img.png" width="150" height="50" priority>
888+
<img ngSrc="path/img.png" width="150" height="50" priority>
889+
<img ngSrc="path/img.png" width="150" height="50" priority>
890+
<img ngSrc="path/img.png" width="150" height="50" priority>
891+
<img ngSrc="path/img.png" width="150" height="50" priority>
892+
<img ngSrc="path/img.png" width="150" height="50" priority>
893+
<img ngSrc="path/img.png" width="150" height="50" priority>
894+
<img ngSrc="path/img.png" width="150" height="50" priority>
895+
<img ngSrc="path/img.png" width="150" height="50" priority>
896+
<img ngSrc="path/img.png" width="150" height="50" priority>
897+
`;
898+
const consoleWarnSpy = spyOn(console, 'warn');
899+
900+
const fixture = createTestComponent(template);
901+
fixture.detectChanges();
902+
903+
// We manually fire the event that is listened by the directive
904+
// as it won't fire in the context of our unit test
905+
document.dispatchEvent(new Event('DOMContentLoaded'));
906+
907+
if (isBrowser) {
908+
expect(consoleWarnSpy.calls.count()).toBe(1);
909+
expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch(
910+
new RegExp(`NG0${RuntimeErrorCode.TOO_MANY_PRIORITY_ATTRIBUTES}`),
911+
);
912+
} else {
913+
// The warning is only logged on browsers
914+
expect(consoleWarnSpy.calls.count()).toBe(0);
915+
}
916+
});
917+
});
873918
});
874919

875920
describe('meta data', () => {

0 commit comments

Comments
 (0)