Prevent blinking on reader page switch on firefox

on firefox images are decoded async which causes a "flicker/blinking" when they're getting visible for the first time
this is an issue especially in the reader because pages that should not be shown are rendered but
not displayed, which then causes this issue once they get displayed
This commit is contained in:
schroda
2024-11-13 14:27:00 +01:00
parent f9938edb75
commit 87ee1330d4
2 changed files with 94 additions and 11 deletions

View File

@@ -859,7 +859,54 @@ export class RequestManager {
return `${this.getValidUrlFor(imageUrl, apiVersion)}`; return `${this.getValidUrlFor(imageUrl, apiVersion)}`;
} }
private fetchImageViaTag(url: string, priority?: QueuePriority): ImageRequest { /**
* Aborts pending image requests
*
* prevents aborting image requests that are already in progress
* e.g. for source image requests, ongoing requests are already handled by the server and aborting them
* will just cause new source image requests to be sent to the server, which then will cause the server
* to become really slow for image requests to the same source
*/
private abortImageRequest(key: string, abort: () => void): void {
if (this.imageQueue.isProcessing(key)) {
return;
}
abort();
}
private async optionallyDecodeImage(url: string, shouldDecode?: boolean): Promise<string> {
if (!shouldDecode) {
return url;
}
const decodePromise = new ControlledPromise();
const img = new Image();
img.src = url;
img.onload = async () => {
try {
await img.decode();
} catch (error) {
decodePromise.reject(error);
}
decodePromise.resolve();
};
img.onerror = (error) => decodePromise.reject(error);
img.onabort = (error) => decodePromise.reject(error);
await decodePromise.promise;
return url;
}
private fetchImageViaTag(
url: string,
{ priority, shouldDecode }: { priority?: QueuePriority; shouldDecode?: boolean } = {},
): ImageRequest {
const imgRequest = new ControlledPromise<string>(); const imgRequest = new ControlledPromise<string>();
imgRequest.promise.catch(() => {}); imgRequest.promise.catch(() => {});
@@ -880,7 +927,15 @@ export class RequestManager {
img.src = url; img.src = url;
img.onload = () => imgRequest.resolve(url); img.onload = async () => {
try {
await this.optionallyDecodeImage(url, shouldDecode);
imgRequest.resolve(url);
} catch (error) {
imgRequest.reject(error);
}
};
img.onerror = (error) => imgRequest.reject(error); img.onerror = (error) => imgRequest.reject(error);
img.onabort = (error) => imgRequest.reject(error); img.onabort = (error) => imgRequest.reject(error);
@@ -909,7 +964,10 @@ export class RequestManager {
* img.src = imageUrl; * img.src = imageUrl;
* *
*/ */
private fetchImageViaFetchApi(url: string, priority?: QueuePriority): ImageRequest { private fetchImageViaFetchApi(
url: string,
{ priority, shouldDecode }: { priority?: QueuePriority; shouldDecode?: boolean } = {},
): ImageRequest {
let objectUrl: string = ''; let objectUrl: string = '';
const { abortRequest, signal } = this.createAbortController(); const { abortRequest, signal } = this.createAbortController();
const { key, promise: response } = this.imageQueue.enqueue( const { key, promise: response } = this.imageQueue.enqueue(
@@ -920,14 +978,16 @@ export class RequestManager {
checkResponseIsJson: false, checkResponseIsJson: false,
config: { config: {
signal, signal,
// @ts-ignore - typing has not been updated yet
priority: 'low', priority: 'low',
}, },
}) })
.then((data) => data.blob()) .then((data) => data.blob())
.then((data) => URL.createObjectURL(data)) .then((data) => URL.createObjectURL(data))
.then((imageUrl) => { .then(async (imageUrl) => {
objectUrl = imageUrl; objectUrl = imageUrl;
await this.optionallyDecodeImage(imageUrl, shouldDecode);
return imageUrl; return imageUrl;
}), }),
priority, priority,
@@ -942,13 +1002,27 @@ export class RequestManager {
/** /**
* Make sure to call "cleanup" once the image is not needed anymore (only required if fetched via "fetch api") * Make sure to call "cleanup" once the image is not needed anymore (only required if fetched via "fetch api")
*
* options:
* - shouldDecode: decodes the image in case the browser is Firefox to prevent a flickering/blinking when an image gets visible for the first time
*/ */
public requestImage(url: string, priority?: QueuePriority, useFetchApi: boolean = false): ImageRequest { public requestImage(
if (useFetchApi) { url: string,
return this.fetchImageViaFetchApi(url, priority); options: { priority?: QueuePriority; useFetchApi?: boolean; shouldDecode?: boolean } = {
useFetchApi: false,
shouldDecode: false,
},
): ImageRequest {
// on firefox images are decoded async which causes a "flicker/blinking" when they're getting visible for the first time
// this is an issue especially in the reader because pages that should not be shown are rendered but
// not displayed, which then causes this issue once they get displayed
const shouldDecode = !!options.shouldDecode && navigator.userAgent.toLowerCase().includes('firefox');
if (options.useFetchApi) {
return this.fetchImageViaFetchApi(url, { ...options, shouldDecode });
} }
return this.fetchImageViaTag(url, priority); return this.fetchImageViaTag(url, { ...options, shouldDecode });
} }
private doRequest<Data, Variables extends OperationVariables = OperationVariables>( private doRequest<Data, Variables extends OperationVariables = OperationVariables>(

View File

@@ -28,11 +28,20 @@ interface IProps {
onImageLoad?: () => void; onImageLoad?: () => void;
shouldDecode?: boolean;
useFetchApi?: boolean; useFetchApi?: boolean;
} }
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => { export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
const { useFetchApi, src, alt, onImageLoad, spinnerStyle: { small, ...spinnerStyle } = {}, imgStyle } = props; const {
shouldDecode,
useFetchApi,
src,
alt,
onImageLoad,
spinnerStyle: { small, ...spinnerStyle } = {},
imgStyle,
} = props;
const { t } = useTranslation(); const { t } = useTranslation();
@@ -57,7 +66,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
return () => {}; return () => {};
} }
const imageRequest = requestManager.requestImage(src, Priority.HIGH, useFetchApi); const imageRequest = requestManager.requestImage(src, { priority: Priority.HIGH, shouldDecode, useFetchApi });
let cacheTimeout: NodeJS.Timeout; let cacheTimeout: NodeJS.Timeout;
const fetchImage = async () => { const fetchImage = async () => {