fix(common): execute checks and remove placeholder when image is already loaded (#55444)

With this commit, we're now able to perform checks even when the image has already
been loaded (e.g., from the browser cache), and its `load` event would never be triggered.
We use the [complete](https://html.spec.whatwg.org/#dom-img-complete) property, as specified,
which indicates that the image state is fully available when the user agent has retrieved all
the image data. This approach effectively triggers checks, as we no longer solely rely on the
`load` event and consider that the image may already be loaded.

This will not remove the placeholder until the `load` event fires (and it won't fire if the
image is already "there").

This prevents memory leaks in development mode, as `load` and `error` event listeners are
still attached to the image element.

PR Close #55444
This commit is contained in:
arturovt 2024-04-20 14:37:13 +03:00 committed by Alex Rickabaugh
parent 1549afe10e
commit c3115b882e

View file

@ -708,6 +708,8 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
const removeLoadListenerFn = this.renderer.listen(img, 'load', callback);
const removeErrorListenerFn = this.renderer.listen(img, 'error', callback);
callOnLoadIfImageIsLoaded(img, callback);
}
/** @nodoc */
@ -1025,7 +1027,7 @@ function assertNoImageDistortion(
img: HTMLImageElement,
renderer: Renderer2,
) {
const removeLoadListenerFn = renderer.listen(img, 'load', () => {
const callback = () => {
removeLoadListenerFn();
removeErrorListenerFn();
const computedStyle = window.getComputedStyle(img);
@ -1118,7 +1120,9 @@ function assertNoImageDistortion(
);
}
}
});
};
const removeLoadListenerFn = renderer.listen(img, 'load', callback);
// We only listen to the `error` event to remove the `load` event listener because it will not be
// fired if the image fails to load. This is done to prevent memory leaks in development mode
@ -1128,6 +1132,8 @@ function assertNoImageDistortion(
removeLoadListenerFn();
removeErrorListenerFn();
});
callOnLoadIfImageIsLoaded(img, callback);
}
/**
@ -1173,7 +1179,7 @@ function assertNonZeroRenderedHeight(
img: HTMLImageElement,
renderer: Renderer2,
) {
const removeLoadListenerFn = renderer.listen(img, 'load', () => {
const callback = () => {
removeLoadListenerFn();
removeErrorListenerFn();
const renderedHeight = img.clientHeight;
@ -1189,13 +1195,17 @@ function assertNonZeroRenderedHeight(
),
);
}
});
};
const removeLoadListenerFn = renderer.listen(img, 'load', callback);
// See comments in the `assertNoImageDistortion`.
const removeErrorListenerFn = renderer.listen(img, 'error', () => {
removeLoadListenerFn();
removeErrorListenerFn();
});
callOnLoadIfImageIsLoaded(img, callback);
}
/**
@ -1338,6 +1348,22 @@ function assertPlaceholderDimensions(dir: NgOptimizedImage, imgElement: HTMLImag
}
}
function callOnLoadIfImageIsLoaded(img: HTMLImageElement, callback: VoidFunction): void {
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-complete
// The spec defines that `complete` is truthy once its request state is fully available.
// The image may already be available if its loaded from the browser cache.
// In that case, the `load` event will not fire at all, meaning that all setup
// callbacks listening for the `load` event will not be invoked.
// In Safari, there is a known behavior where the `complete` property of an
// `HTMLImageElement` may sometimes return `true` even when the image is not fully loaded.
// Checking both `img.complete` and `img.naturalWidth` is the most reliable way to
// determine if an image has been fully loaded, especially in browsers where the
// `complete` property may return `true` prematurely.
if (img.complete && img.naturalWidth) {
callback();
}
}
function round(input: number): number | string {
return Number.isInteger(input) ? input : input.toFixed(2);
}