Skip to content

Commit

Permalink
feat(common): warn if rendered size is much smaller than intrinsic (#…
Browse files Browse the repository at this point in the history
…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 #47082
  • Loading branch information
kara authored and Pawel Kozlowski committed Aug 16, 2022
1 parent 0f6b30b commit f81765b
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 23 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/common/errors.md
Expand Up @@ -19,6 +19,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
NG_FOR_MISSING_DIFFER = -2200,
// (undocumented)
OVERSIZED_IMAGE = 2960,
// (undocumented)
PARENT_NG_SWITCH_NOT_FOUND = 2000,
// (undocumented)
PRIORITY_IMG_MISSING_PRECONNECT_TAG = 2956,
Expand Down
Expand Up @@ -54,6 +54,13 @@ export const RECOMMENDED_SRCSET_DENSITY_CAP = 2;
*/
const ASPECT_RATIO_TOLERANCE = .1;

/**
* Used to determine whether the image has been requested at an overly
* large size compared to the actual rendered image size (after taking
* into account a typical device pixel ratio). In pixels.
*/
const OVERSIZED_IMAGE_TOLERANCE = 1000;

/**
* Directive that improves image loading performance by enforcing best practices.
*
Expand Down Expand Up @@ -587,6 +594,7 @@ function assertNoImageDistortion(
Math.abs(suppliedAspectRatio - intrinsicAspectRatio) > ASPECT_RATIO_TOLERANCE;
const stylingDistortion = nonZeroRenderedDimensions &&
Math.abs(intrinsicAspectRatio - renderedAspectRatio) > ASPECT_RATIO_TOLERANCE;

if (inaccurateDimensions) {
console.warn(formatRuntimeError(
RuntimeErrorCode.INVALID_INPUT,
Expand All @@ -596,20 +604,36 @@ function assertNoImageDistortion(
`(aspect-ratio: ${intrinsicAspectRatio}). Supplied width and height attributes: ` +
`${suppliedWidth}w x ${suppliedHeight}h (aspect-ratio: ${suppliedAspectRatio}). ` +
`To fix this, update the width and height attributes.`));
} else {
if (stylingDistortion) {
} else if (stylingDistortion) {
console.warn(formatRuntimeError(
RuntimeErrorCode.INVALID_INPUT,
`${imgDirectiveDetails(dir.rawSrc)} the aspect ratio of the rendered image ` +
`does not match the image's intrinsic aspect ratio. ` +
`Intrinsic image size: ${intrinsicWidth}w x ${intrinsicHeight}h ` +
`(aspect-ratio: ${intrinsicAspectRatio}). Rendered image size: ` +
`${renderedWidth}w x ${renderedHeight}h (aspect-ratio: ` +
`${renderedAspectRatio}). This issue can occur if "width" and "height" ` +
`attributes are added to an image without updating the corresponding ` +
`image styling. To fix this, adjust image styling. In most cases, ` +
`adding "height: auto" or "width: auto" to the image styling will fix ` +
`this issue.`));
} else if (!dir.rawSrcset && nonZeroRenderedDimensions) {
// If `rawSrcset` hasn't been set, sanity check the intrinsic size.
const recommendedWidth = RECOMMENDED_SRCSET_DENSITY_CAP * renderedWidth;
const recommendedHeight = RECOMMENDED_SRCSET_DENSITY_CAP * renderedHeight;
const oversizedWidth = (intrinsicWidth - recommendedWidth) >= OVERSIZED_IMAGE_TOLERANCE;
const oversizedHeight = (intrinsicHeight - recommendedHeight) >= OVERSIZED_IMAGE_TOLERANCE;
if (oversizedWidth || oversizedHeight) {
console.warn(formatRuntimeError(
RuntimeErrorCode.INVALID_INPUT,
`${imgDirectiveDetails(dir.rawSrc)} the aspect ratio of the rendered image ` +
`does not match the image's intrinsic aspect ratio. ` +
`Intrinsic image size: ${intrinsicWidth}w x ${intrinsicHeight}h ` +
`(aspect-ratio: ${intrinsicAspectRatio}). Rendered image size: ` +
`${renderedWidth}w x ${renderedHeight}h (aspect-ratio: ` +
`${renderedAspectRatio}). This issue can occur if "width" and "height" ` +
`attributes are added to an image without updating the corresponding ` +
`image styling. To fix this, adjust image styling. In most cases, ` +
`adding "height: auto" or "width: auto" to the image styling will fix ` +
`this issue.`));
RuntimeErrorCode.OVERSIZED_IMAGE,
`${imgDirectiveDetails(dir.rawSrc)} the intrinsic image is significantly ` +
`larger than necessary. ` +
`Rendered image size: ${renderedWidth}w x ${renderedHeight}h. ` +
`Intrinsic image size: ${intrinsicWidth}w x ${intrinsicHeight}h. ` +
`Recommended intrinsic image size: ${recommendedWidth}w x ${recommendedHeight}h. ` +
`Note: Recommended intrinsic image size is calculated assuming a maximum DPR of ` +
`${RECOMMENDED_SRCSET_DENSITY_CAP}. To improve loading time, resize the image ` +
`or consider using the "rawSrcset" and "sizes" attributes.`));
}
}
});
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/errors.ts
Expand Up @@ -31,4 +31,5 @@ export const enum RuntimeErrorCode {
INVALID_PRECONNECT_CHECK_BLOCKLIST = 2957,
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
INVALID_LOADER_ARGUMENTS = 2959,
OVERSIZED_IMAGE = 2960,
}
2 changes: 2 additions & 0 deletions packages/core/test/bundling/image-directive/BUILD.bazel
Expand Up @@ -8,6 +8,7 @@ ng_module(
"e2e/basic/basic.ts",
"e2e/image-distortion/image-distortion.ts",
"e2e/lcp-check/lcp-check.ts",
"e2e/oversized-image/oversized-image.ts",
"e2e/preconnect-check/preconnect-check.ts",
"index.ts",
"playground.ts",
Expand Down Expand Up @@ -59,6 +60,7 @@ ts_devserver(
"e2e/a.png",
"e2e/b.png",
"e2e/logo-500w.jpg",
"e2e/logo-1500w.jpg",
],
deps = [":image-directive"],
)
Expand Down
Expand Up @@ -18,7 +18,7 @@ import {Component} from '@angular/core';
<!-- This image is here for the sake of making sure the "LCP image is priority" assertion is passed -->
<img rawSrc="/e2e/logo-500w.jpg" width="500" height="500" priority>
<br>
<!-- width and height attributes exacly match the intrinsic size of image -->
<!-- width and height attributes exactly match the intrinsic size of image -->
<img rawSrc="/e2e/a.png" width="25" height="25">
<br>
<!-- supplied aspect ratio exactly matches intrinsic aspect ratio-->
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/core/test/bundling/image-directive/e2e/logo-500w.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,31 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/* tslint:disable:no-console */
import {browser, by, element, ExpectedConditions} from 'protractor';
import {logging} from 'selenium-webdriver';

import {collectBrowserLogs} from '../util';

describe('NgOptimizedImage directive', () => {
it('should not warn if there is no oversized image', async () => {
await browser.get('/e2e/oversized-image-passing');
const logs = await collectBrowserLogs(logging.Level.WARNING);
expect(logs.length).toEqual(0);
});

it('should warn if rendered image size is much smaller than intrinsic size', async () => {
await browser.get('/e2e/oversized-image-failing');
const logs = await collectBrowserLogs(logging.Level.WARNING);

expect(logs.length).toEqual(1);

const expectedMessageRegex = /the intrinsic image is significantly larger than necessary\./;
expect(expectedMessageRegex.test(logs[0].message)).toBeTruthy();
});
});
@@ -0,0 +1,44 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {NgOptimizedImage} from '@angular/common';
import {Component} from '@angular/core';

@Component({
selector: 'oversized-image-passing',
standalone: true,
imports: [NgOptimizedImage],
template: `
<!-- Image is rendered within threshold range-->
<div style="width: 500px; height: 500px">
<img rawSrc="/e2e/logo-500w.jpg" width="200" height="200" priority>
</div>
<!-- Image is rendered too small but rawSrcset set-->
<div style="width: 300px; height: 300px">
<img rawSrc="/e2e/logo-1500w.jpg" width="100" height="100" priority
rawSrcset="100w, 200w">
</div>
`,
})
export class OversizedImageComponentPassing {
}


@Component({
selector: 'oversized-image-failing',
standalone: true,
imports: [NgOptimizedImage],
template: `
<!-- Image is rendered too small -->
<div style="width: 300px; height: 300px">
<img rawSrc="/e2e/logo-1500w.jpg" width="100" height="100" priority>
</div>
`,
})
export class OversizedImageComponentFailing {
}
Expand Up @@ -38,13 +38,14 @@ describe('NgOptimizedImage directive', () => {
expect(logs[0].message).toMatch(/NG02956.*?a\.png/);
});

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

await verifyImagesPresent(element);
await verifyImagesPresent(element);

// Make sure there are no browser logs.
const logs = await collectBrowserLogs(logging.Level.WARNING);
expect(logs.length).toEqual(0);
});
// Make sure there are no browser logs.
const logs = await collectBrowserLogs(logging.Level.WARNING);
expect(logs.length).toEqual(0);
});
});
3 changes: 3 additions & 0 deletions packages/core/test/bundling/image-directive/index.ts
Expand Up @@ -13,6 +13,7 @@ import {RouterModule} from '@angular/router';
import {BasicComponent} from './e2e/basic/basic';
import {ImageDistortionFailingComponent, ImageDistortionPassingComponent} from './e2e/image-distortion/image-distortion';
import {LcpCheckComponent} from './e2e/lcp-check/lcp-check';
import {OversizedImageComponentFailing, OversizedImageComponentPassing} from './e2e/oversized-image/oversized-image';
import {PreconnectCheckComponent} from './e2e/preconnect-check/preconnect-check';
import {PlaygroundComponent} from './playground';

Expand All @@ -35,6 +36,8 @@ const ROUTES = [
{path: 'e2e/preconnect-check', component: PreconnectCheckComponent},
{path: 'e2e/image-distortion-passing', component: ImageDistortionPassingComponent},
{path: 'e2e/image-distortion-failing', component: ImageDistortionFailingComponent},
{path: 'e2e/oversized-image-passing', component: OversizedImageComponentPassing},
{path: 'e2e/oversized-image-failing', component: OversizedImageComponentFailing},
];

bootstrapApplication(RootComponent, {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/bundling/image-directive/playground.ts
Expand Up @@ -34,12 +34,12 @@ import {Component} from '@angular/core';
`],
template: `
<h1>
<img rawSrc="a.png" width="50" height="50" priority>
<img rawSrc="a.png" width="50" height="50" priority rawSrcset="1x, 2x">
<span>Angular image app</span>
</h1>
<main>
<div class="spacer"></div>
<img rawSrc="hermes.jpeg" rawSrcset="100w, 200w, 1000w" width="4030" height="3020">
<img rawSrc="hermes2.jpeg" rawSrcset="100w, 200w, 1000w, 2000w" width="1791" height="1008">
</main>
`,
standalone: true,
Expand Down

0 comments on commit f81765b

Please sign in to comment.