Skip to content

Commit f81765b

Browse files
karaPawel Kozlowski
authored andcommitted
feat(common): warn if rendered size is much smaller than intrinsic (angular#47082)
This commit adds a console warning in development mode if the ultimate rendered size of the image is much smaller than the dimensions of the requested image. In this case, the warning recommends adjusting the size of the source image or using the `rawSrcset` attribute to implement responsive sizing. PR Close angular#47082
1 parent 0f6b30b commit f81765b

File tree

12 files changed

+131
-23
lines changed

12 files changed

+131
-23
lines changed

goldens/public-api/common/errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const enum RuntimeErrorCode {
1919
// (undocumented)
2020
NG_FOR_MISSING_DIFFER = -2200,
2121
// (undocumented)
22+
OVERSIZED_IMAGE = 2960,
23+
// (undocumented)
2224
PARENT_NG_SWITCH_NOT_FOUND = 2000,
2325
// (undocumented)
2426
PRIORITY_IMG_MISSING_PRECONNECT_TAG = 2956,

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

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export const RECOMMENDED_SRCSET_DENSITY_CAP = 2;
5454
*/
5555
const ASPECT_RATIO_TOLERANCE = .1;
5656

57+
/**
58+
* Used to determine whether the image has been requested at an overly
59+
* large size compared to the actual rendered image size (after taking
60+
* into account a typical device pixel ratio). In pixels.
61+
*/
62+
const OVERSIZED_IMAGE_TOLERANCE = 1000;
63+
5764
/**
5865
* Directive that improves image loading performance by enforcing best practices.
5966
*
@@ -587,6 +594,7 @@ function assertNoImageDistortion(
587594
Math.abs(suppliedAspectRatio - intrinsicAspectRatio) > ASPECT_RATIO_TOLERANCE;
588595
const stylingDistortion = nonZeroRenderedDimensions &&
589596
Math.abs(intrinsicAspectRatio - renderedAspectRatio) > ASPECT_RATIO_TOLERANCE;
597+
590598
if (inaccurateDimensions) {
591599
console.warn(formatRuntimeError(
592600
RuntimeErrorCode.INVALID_INPUT,
@@ -596,20 +604,36 @@ function assertNoImageDistortion(
596604
`(aspect-ratio: ${intrinsicAspectRatio}). Supplied width and height attributes: ` +
597605
`${suppliedWidth}w x ${suppliedHeight}h (aspect-ratio: ${suppliedAspectRatio}). ` +
598606
`To fix this, update the width and height attributes.`));
599-
} else {
600-
if (stylingDistortion) {
607+
} else if (stylingDistortion) {
608+
console.warn(formatRuntimeError(
609+
RuntimeErrorCode.INVALID_INPUT,
610+
`${imgDirectiveDetails(dir.rawSrc)} the aspect ratio of the rendered image ` +
611+
`does not match the image's intrinsic aspect ratio. ` +
612+
`Intrinsic image size: ${intrinsicWidth}w x ${intrinsicHeight}h ` +
613+
`(aspect-ratio: ${intrinsicAspectRatio}). Rendered image size: ` +
614+
`${renderedWidth}w x ${renderedHeight}h (aspect-ratio: ` +
615+
`${renderedAspectRatio}). This issue can occur if "width" and "height" ` +
616+
`attributes are added to an image without updating the corresponding ` +
617+
`image styling. To fix this, adjust image styling. In most cases, ` +
618+
`adding "height: auto" or "width: auto" to the image styling will fix ` +
619+
`this issue.`));
620+
} else if (!dir.rawSrcset && nonZeroRenderedDimensions) {
621+
// If `rawSrcset` hasn't been set, sanity check the intrinsic size.
622+
const recommendedWidth = RECOMMENDED_SRCSET_DENSITY_CAP * renderedWidth;
623+
const recommendedHeight = RECOMMENDED_SRCSET_DENSITY_CAP * renderedHeight;
624+
const oversizedWidth = (intrinsicWidth - recommendedWidth) >= OVERSIZED_IMAGE_TOLERANCE;
625+
const oversizedHeight = (intrinsicHeight - recommendedHeight) >= OVERSIZED_IMAGE_TOLERANCE;
626+
if (oversizedWidth || oversizedHeight) {
601627
console.warn(formatRuntimeError(
602-
RuntimeErrorCode.INVALID_INPUT,
603-
`${imgDirectiveDetails(dir.rawSrc)} the aspect ratio of the rendered image ` +
604-
`does not match the image's intrinsic aspect ratio. ` +
605-
`Intrinsic image size: ${intrinsicWidth}w x ${intrinsicHeight}h ` +
606-
`(aspect-ratio: ${intrinsicAspectRatio}). Rendered image size: ` +
607-
`${renderedWidth}w x ${renderedHeight}h (aspect-ratio: ` +
608-
`${renderedAspectRatio}). This issue can occur if "width" and "height" ` +
609-
`attributes are added to an image without updating the corresponding ` +
610-
`image styling. To fix this, adjust image styling. In most cases, ` +
611-
`adding "height: auto" or "width: auto" to the image styling will fix ` +
612-
`this issue.`));
628+
RuntimeErrorCode.OVERSIZED_IMAGE,
629+
`${imgDirectiveDetails(dir.rawSrc)} the intrinsic image is significantly ` +
630+
`larger than necessary. ` +
631+
`Rendered image size: ${renderedWidth}w x ${renderedHeight}h. ` +
632+
`Intrinsic image size: ${intrinsicWidth}w x ${intrinsicHeight}h. ` +
633+
`Recommended intrinsic image size: ${recommendedWidth}w x ${recommendedHeight}h. ` +
634+
`Note: Recommended intrinsic image size is calculated assuming a maximum DPR of ` +
635+
`${RECOMMENDED_SRCSET_DENSITY_CAP}. To improve loading time, resize the image ` +
636+
`or consider using the "rawSrcset" and "sizes" attributes.`));
613637
}
614638
}
615639
});

packages/common/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ export const enum RuntimeErrorCode {
3131
INVALID_PRECONNECT_CHECK_BLOCKLIST = 2957,
3232
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
3333
INVALID_LOADER_ARGUMENTS = 2959,
34+
OVERSIZED_IMAGE = 2960,
3435
}

packages/core/test/bundling/image-directive/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ng_module(
88
"e2e/basic/basic.ts",
99
"e2e/image-distortion/image-distortion.ts",
1010
"e2e/lcp-check/lcp-check.ts",
11+
"e2e/oversized-image/oversized-image.ts",
1112
"e2e/preconnect-check/preconnect-check.ts",
1213
"index.ts",
1314
"playground.ts",
@@ -59,6 +60,7 @@ ts_devserver(
5960
"e2e/a.png",
6061
"e2e/b.png",
6162
"e2e/logo-500w.jpg",
63+
"e2e/logo-1500w.jpg",
6264
],
6365
deps = [":image-directive"],
6466
)

packages/core/test/bundling/image-directive/e2e/image-distortion/image-distortion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {Component} from '@angular/core';
1818
<!-- This image is here for the sake of making sure the "LCP image is priority" assertion is passed -->
1919
<img rawSrc="/e2e/logo-500w.jpg" width="500" height="500" priority>
2020
<br>
21-
<!-- width and height attributes exacly match the intrinsic size of image -->
21+
<!-- width and height attributes exactly match the intrinsic size of image -->
2222
<img rawSrc="/e2e/a.png" width="25" height="25">
2323
<br>
2424
<!-- supplied aspect ratio exactly matches intrinsic aspect ratio-->
54.1 KB
Loading
-2.3 KB
Loading
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/* tslint:disable:no-console */
10+
import {browser, by, element, ExpectedConditions} from 'protractor';
11+
import {logging} from 'selenium-webdriver';
12+
13+
import {collectBrowserLogs} from '../util';
14+
15+
describe('NgOptimizedImage directive', () => {
16+
it('should not warn if there is no oversized image', async () => {
17+
await browser.get('/e2e/oversized-image-passing');
18+
const logs = await collectBrowserLogs(logging.Level.WARNING);
19+
expect(logs.length).toEqual(0);
20+
});
21+
22+
it('should warn if rendered image size is much smaller than intrinsic size', async () => {
23+
await browser.get('/e2e/oversized-image-failing');
24+
const logs = await collectBrowserLogs(logging.Level.WARNING);
25+
26+
expect(logs.length).toEqual(1);
27+
28+
const expectedMessageRegex = /the intrinsic image is significantly larger than necessary\./;
29+
expect(expectedMessageRegex.test(logs[0].message)).toBeTruthy();
30+
});
31+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgOptimizedImage} from '@angular/common';
10+
import {Component} from '@angular/core';
11+
12+
@Component({
13+
selector: 'oversized-image-passing',
14+
standalone: true,
15+
imports: [NgOptimizedImage],
16+
template: `
17+
<!-- Image is rendered within threshold range-->
18+
<div style="width: 500px; height: 500px">
19+
<img rawSrc="/e2e/logo-500w.jpg" width="200" height="200" priority>
20+
</div>
21+
<!-- Image is rendered too small but rawSrcset set-->
22+
<div style="width: 300px; height: 300px">
23+
<img rawSrc="/e2e/logo-1500w.jpg" width="100" height="100" priority
24+
rawSrcset="100w, 200w">
25+
</div>
26+
`,
27+
})
28+
export class OversizedImageComponentPassing {
29+
}
30+
31+
32+
@Component({
33+
selector: 'oversized-image-failing',
34+
standalone: true,
35+
imports: [NgOptimizedImage],
36+
template: `
37+
<!-- Image is rendered too small -->
38+
<div style="width: 300px; height: 300px">
39+
<img rawSrc="/e2e/logo-1500w.jpg" width="100" height="100" priority>
40+
</div>
41+
`,
42+
})
43+
export class OversizedImageComponentFailing {
44+
}

packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.e2e-spec.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ describe('NgOptimizedImage directive', () => {
3838
expect(logs[0].message).toMatch(/NG02956.*?a\.png/);
3939
});
4040

41-
it('should not produce any warnings in the console when a preconect tag is present', async () => {
42-
await browser.get('/e2e/preconnect-check?preconnect');
41+
it('should not produce any warnings in the console when a preconnect tag is present',
42+
async () => {
43+
await browser.get('/e2e/preconnect-check?preconnect');
4344

44-
await verifyImagesPresent(element);
45+
await verifyImagesPresent(element);
4546

46-
// Make sure there are no browser logs.
47-
const logs = await collectBrowserLogs(logging.Level.WARNING);
48-
expect(logs.length).toEqual(0);
49-
});
47+
// Make sure there are no browser logs.
48+
const logs = await collectBrowserLogs(logging.Level.WARNING);
49+
expect(logs.length).toEqual(0);
50+
});
5051
});

0 commit comments

Comments
 (0)