Skip to content

Commit

Permalink
fix(common): execute checks when image is already loaded
Browse files Browse the repository at this point in the history
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 utilize 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. Additionally, it helps prevent
memory leaks in development mode, as `load` and `error` event listeners will still be attached
to the image element.
  • Loading branch information
arturovt committed Apr 20, 2024
1 parent 3bc63ea commit 0d687eb
Showing 1 changed file with 29 additions and 4 deletions.
Expand Up @@ -659,6 +659,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 */
Expand Down Expand Up @@ -976,7 +978,7 @@ function assertNoImageDistortion(
img: HTMLImageElement,
renderer: Renderer2,
) {
const removeLoadListenerFn = renderer.listen(img, 'load', () => {
const callback = () => {
removeLoadListenerFn();
removeErrorListenerFn();
const computedStyle = window.getComputedStyle(img);
Expand Down Expand Up @@ -1069,7 +1071,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
Expand All @@ -1079,6 +1083,8 @@ function assertNoImageDistortion(
removeLoadListenerFn();
removeErrorListenerFn();
});

callOnLoadIfImageIsLoaded(img, callback);
}

/**
Expand Down Expand Up @@ -1124,7 +1130,7 @@ function assertNonZeroRenderedHeight(
img: HTMLImageElement,
renderer: Renderer2,
) {
const removeLoadListenerFn = renderer.listen(img, 'load', () => {
const callback = () => {
removeLoadListenerFn();
removeErrorListenerFn();
const renderedHeight = img.clientHeight;
Expand All @@ -1140,13 +1146,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);
}

/**
Expand Down Expand Up @@ -1245,6 +1255,21 @@ function assertNoLoaderParamsWithoutLoader(dir: NgOptimizedImage, imageLoader: I
}
}

function callOnLoadIfImageIsLoaded(img: HTMLImageElement, callback: VoidFunction): void {
// Note that the image may already be loaded from the browser cache before the
// `load` event fires, and the `load` event will not fire if it's already `complete`.
// 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.
// Mozilla employs the same check internally in its codebase, verifying
// both `complete` and `naturalWidth` when setting up a `load` event listener.
if (img.complete && img.naturalWidth) {
callback();
}
}

function round(input: number): number | string {
return Number.isInteger(input) ? input : input.toFixed(2);
}
Expand Down

0 comments on commit 0d687eb

Please sign in to comment.