Improve memoization of page rendering

This commit is contained in:
schroda
2024-12-22 00:42:33 +01:00
parent 58c402dd0e
commit 34ad331a71
8 changed files with 178 additions and 135 deletions

View File

@@ -6,10 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { ComponentProps, ForwardedRef, forwardRef, memo } from 'react'; import { ComponentProps, memo, useCallback } from 'react';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { IReaderSettings, ReaderCustomFilter, TReaderScrollbarContext } from '@/modules/reader/types/Reader.types.ts'; import {
IReaderSettings,
ReaderCustomFilter,
ReaderPagerProps,
TReaderScrollbarContext,
} from '@/modules/reader/types/Reader.types.ts';
import { import {
getImageMarginStyling, getImageMarginStyling,
getImagePlaceholderStyling, getImagePlaceholderStyling,
@@ -49,9 +54,10 @@ const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
}) })
.join(' '); .join(' ');
const BaseReaderPage = forwardRef( const BaseReaderPage = ({
( pageIndex,
{ pagesIndex,
isPrimaryPage,
display, display,
doublePage = false, doublePage = false,
position, position,
@@ -64,22 +70,30 @@ const BaseReaderPage = forwardRef(
readerWidth, readerWidth,
scrollbarXSize, scrollbarXSize,
scrollbarYSize, scrollbarYSize,
onLoad,
onError,
setRef,
...props ...props
}: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle'> & }: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle' | 'onLoad' | 'onError'> &
Pick< Pick<IReaderSettings, 'readingMode' | 'customFilter' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth'> &
IReaderSettings,
'readingMode' | 'customFilter' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth'
> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & { Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & {
pageIndex: number;
pagesIndex: number;
isPrimaryPage: boolean;
display: boolean; display: boolean;
doublePage?: boolean; doublePage?: boolean;
position?: 'left' | 'right'; position?: 'left' | 'right';
marginTop?: number; marginTop?: number;
}, onLoad: ReaderPagerProps['onLoad'];
ref: ForwardedRef<HTMLImageElement | null>, onError: ReaderPagerProps['onError'];
) => { setRef?: (pagesIndex: number, ref: HTMLElement | null) => void;
}) => {
const isTabletWidth = MediaQuery.useIsTabletWidth(); const isTabletWidth = MediaQuery.useIsTabletWidth();
const handleLoad = useCallback(() => onLoad?.(pagesIndex, isPrimaryPage), [onLoad, pagesIndex, isPrimaryPage]);
const handleError = useCallback(() => onError?.(pageIndex), [onError, pageIndex]);
const updateRef = useCallback((element: HTMLElement | null) => setRef?.(pagesIndex, element), [pagesIndex, setRef]);
if (!display && !shouldLoad) { if (!display && !shouldLoad) {
return null; return null;
} }
@@ -87,9 +101,11 @@ const BaseReaderPage = forwardRef(
return ( return (
<SpinnerImage <SpinnerImage
{...props} {...props}
onLoad={handleLoad}
onError={handleError}
shouldLoad={shouldLoad} shouldLoad={shouldLoad}
shouldDecode shouldDecode
ref={ref} ref={updateRef}
spinnerStyle={{ spinnerStyle={{
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
...getImagePlaceholderStyling( ...getImagePlaceholderStyling(
@@ -108,14 +124,7 @@ const BaseReaderPage = forwardRef(
...getImageMarginStyling(doublePage, position), ...getImageMarginStyling(doublePage, position),
}} }}
imgStyle={{ imgStyle={{
...getImageWidthStyling( ...getImageWidthStyling(readingMode, shouldStretchPage, pageScaleMode, doublePage, readerWidth, true),
readingMode,
shouldStretchPage,
pageScaleMode,
doublePage,
readerWidth,
true,
),
display: 'block', display: 'block',
...applyStyles(!display, { ...applyStyles(!display, {
display: 'none', display: 'none',
@@ -138,8 +147,7 @@ const BaseReaderPage = forwardRef(
}} }}
/> />
); );
}, };
);
export const ReaderPage = withPropsFrom( export const ReaderPage = withPropsFrom(
memo(BaseReaderPage), memo(BaseReaderPage),

View File

@@ -10,6 +10,7 @@ import {
ForwardedRef, ForwardedRef,
forwardRef, forwardRef,
memo, memo,
useCallback,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useLayoutEffect, useLayoutEffect,
@@ -162,6 +163,30 @@ const BaseReaderViewer = forwardRef(
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode]; const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
const onLoad = useCallback(
(pagesIndex: number, isPrimary: boolean = true) => {
const page = actualPages[pagesIndex];
const { index, url } = isPrimary ? page.primary : page.secondary!;
if (readingMode === ReadingMode.DOUBLE_PAGE) {
const img = new Image();
img.onload = () => {
setPagesToSpreadState((prevState) => prevState.toSpliced(index, 1, isSpreadPage(img)));
};
img.src = url;
}
setPageLoadStates((statePageLoadStates) => statePageLoadStates.toSpliced(index, 1, { loaded: true }));
},
[actualPages, readingMode],
);
const onError = useCallback((pageIndex: number) => {
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(pageIndex, 1, { loaded: false, error: true }),
);
}, []);
// reset spread state // reset spread state
useLayoutEffect(() => { useLayoutEffect(() => {
setPagesToSpreadState(Array(totalPages).fill(false)); setPagesToSpreadState(Array(totalPages).fill(false));
@@ -310,27 +335,8 @@ const BaseReaderViewer = forwardRef(
pageLoadStates={pageLoadStates} pageLoadStates={pageLoadStates}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix} retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
imageRefs={imageRefs} imageRefs={imageRefs}
onLoad={(pagesIndex, isPrimary = true) => { onLoad={onLoad}
const page = actualPages[pagesIndex]; onError={onError}
const { index, url } = isPrimary ? page.primary : page.secondary!;
if (readingMode === ReadingMode.DOUBLE_PAGE) {
const img = new Image();
img.onload = () => {
setPagesToSpreadState((prevState) => prevState.toSpliced(index, 1, isSpreadPage(img)));
};
img.src = url;
}
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(index, 1, { loaded: true }),
);
}}
onError={(pageIndex) => {
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(pageIndex, 1, { loaded: false, error: true }),
);
}}
/> />
</Stack> </Stack>
); );

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { memo, ReactNode, useEffect, useMemo, useRef } from 'react'; import { memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
import Box, { BoxProps } from '@mui/material/Box'; import Box, { BoxProps } from '@mui/material/Box';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { getImageWidthStyling, getPageIndexesToLoad } from '@/modules/reader/utils/ReaderPager.utils.tsx'; import { getImageWidthStyling, getPageIndexesToLoad } from '@/modules/reader/utils/ReaderPager.utils.tsx';
@@ -37,7 +37,7 @@ const BaseBasePager = ({
pagesIndex: number, pagesIndex: number,
shouldLoad: boolean, shouldLoad: boolean,
shouldDisplay: boolean, shouldDisplay: boolean,
setRef: (element: HTMLElement | null) => void, setRef: (pagesIndex: number, element: HTMLElement | null) => void,
) => ReactNode; ) => ReactNode;
slots?: { boxProps?: BoxProps }; slots?: { boxProps?: BoxProps };
}) => { }) => {
@@ -50,6 +50,14 @@ const BaseBasePager = ({
previousCurrentPageIndex.current = currentPageIndex; previousCurrentPageIndex.current = currentPageIndex;
}, [pagesIndexesToRender]); }, [pagesIndexesToRender]);
const setRef = useCallback(
(pagesIndex: number, element: HTMLElement | null) => {
// eslint-disable-next-line no-param-reassign
imageRefs.current[pagesIndex] = element;
},
[imageRefs],
);
return ( return (
<Box <Box
{...slots?.boxProps} {...slots?.boxProps}
@@ -70,10 +78,7 @@ const BaseBasePager = ({
pagesIndex, pagesIndex,
pagesIndexesToRender.includes(pagesIndex), pagesIndexesToRender.includes(pagesIndex),
[ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode), [ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode),
(element) => { setRef,
// eslint-disable-next-line no-param-reassign
imageRefs.current[pagesIndex] = element;
},
), ),
)} )}
<ReaderTransitionPage <ReaderTransitionPage

View File

@@ -69,8 +69,10 @@ const BaseReaderDoublePagedPager = ({
<Fragment key={`${primary.url}_${secondary?.url}`}> <Fragment key={`${primary.url}_${secondary?.url}`}>
{createReaderPage( {createReaderPage(
page, page,
() => onLoad?.(pagesIndex), pagesIndex,
() => onError?.(primary.index), true,
onLoad,
onError,
shouldLoad, shouldLoad,
shouldDisplay && isPrimaryPage, shouldDisplay && isPrimaryPage,
currentPage.primary.index, currentPage.primary.index,
@@ -82,8 +84,10 @@ const BaseReaderDoublePagedPager = ({
{hasSecondaryPage && {hasSecondaryPage &&
createReaderPage( createReaderPage(
{ ...page, primary: { ...page.secondary! } }, { ...page, primary: { ...page.secondary! } },
() => onLoad?.(pagesIndex, false), pagesIndex,
() => onError?.(secondary.index), false,
onLoad,
onError,
shouldLoad, shouldLoad,
shouldDisplay && isSecondaryPage, shouldDisplay && isSecondaryPage,
currentSecondaryPageIndex, currentSecondaryPageIndex,

View File

@@ -36,8 +36,10 @@ const BaseReaderHorizontalPager = ({
createPage={(page, pagesIndex, shouldLoad, _, setRef) => createPage={(page, pagesIndex, shouldLoad, _, setRef) =>
createReaderPage( createReaderPage(
page, page,
() => onLoad?.(pagesIndex), pagesIndex,
() => onError?.(page.primary.index), true,
onLoad,
onError,
shouldLoad, shouldLoad,
true, true,
currentPageIndex, currentPageIndex,

View File

@@ -26,8 +26,10 @@ const BaseReaderPagedPager = ({
createPage={(page, pagesIndex, shouldLoad, shouldDisplay) => createPage={(page, pagesIndex, shouldLoad, shouldDisplay) =>
createReaderPage( createReaderPage(
page, page,
() => onLoad?.(pagesIndex), pagesIndex,
() => onError?.(page.primary.index), true,
onLoad,
onError,
shouldLoad, shouldLoad,
shouldDisplay && currentPageIndex === page.primary.index, shouldDisplay && currentPageIndex === page.primary.index,
currentPageIndex, currentPageIndex,

View File

@@ -33,8 +33,10 @@ const BaseReaderVerticalPager = ({
createPage={(page, pagesIndex, shouldLoad, _, setRef) => createPage={(page, pagesIndex, shouldLoad, _, setRef) =>
createReaderPage( createReaderPage(
page, page,
() => onLoad?.(pagesIndex), pagesIndex,
() => onError?.(page.primary.index), true,
onLoad,
onError,
shouldLoad, shouldLoad,
true, true,
currentPageIndex, currentPageIndex,

View File

@@ -7,7 +7,7 @@
*/ */
import { Direction, Theme } from '@mui/material/styles'; import { Direction, Theme } from '@mui/material/styles';
import { ReactNode } from 'react'; import { ComponentProps, ReactNode } from 'react';
import { import {
IReaderSettings, IReaderSettings,
PageInViewportType, PageInViewportType,
@@ -227,7 +227,16 @@ export const createPageData = (url: string, index: number): ReaderStatePages['pa
export const createPagesData = (pageUrls: string[]): ReaderStatePages['pages'] => pageUrls.map(createPageData); export const createPagesData = (pageUrls: string[]): ReaderStatePages['pages'] => pageUrls.map(createPageData);
const getPageDownloadPriority = (currentPageIndex: number, pageIndex: number, totalPages: number): number => { const getPageDownloadPriority = (
currentPageIndex: number,
pageIndex: number,
totalPages: number,
shouldLoad: boolean,
): number => {
if (!shouldLoad) {
return Number.MAX_SAFE_INTEGER;
}
const distanceToCurrentPage = Math.abs(pageIndex - currentPageIndex); const distanceToCurrentPage = Math.abs(pageIndex - currentPageIndex);
const priorityBasedOnDistance = totalPages - distanceToCurrentPage; const priorityBasedOnDistance = totalPages - distanceToCurrentPage;
@@ -241,8 +250,10 @@ const getPageDownloadPriority = (currentPageIndex: number, pageIndex: number, to
export const createReaderPage = ( export const createReaderPage = (
{ primary: { index, alt, url } }: ReaderStatePages['pages'][number], { primary: { index, alt, url } }: ReaderStatePages['pages'][number],
onLoad: () => void, pagesIndex: number,
onError: (() => void) | undefined, isPrimaryPage: boolean,
onLoad: ComponentProps<typeof ReaderPage>['onLoad'],
onError: ComponentProps<typeof ReaderPage>['onError'],
shouldLoad: boolean, shouldLoad: boolean,
display: boolean, display: boolean,
currentPageIndex: number, currentPageIndex: number,
@@ -251,15 +262,18 @@ export const createReaderPage = (
position?: 'left' | 'right', position?: 'left' | 'right',
isDoublePage?: boolean, isDoublePage?: boolean,
marginTop?: number, marginTop?: number,
setRef?: (ref: HTMLElement | null) => void, setRef?: (pagesIndex: number, ref: HTMLElement | null) => void,
): ReactNode => ( ): ReactNode => (
<ReaderPage <ReaderPage
ref={setRef} setRef={setRef}
pageIndex={index}
pagesIndex={pagesIndex}
isPrimaryPage={isPrimaryPage}
key={url} key={url}
src={url} src={url}
alt={alt} alt={alt}
display={display} display={display}
priority={getPageDownloadPriority(currentPageIndex, index, totalPages)} priority={getPageDownloadPriority(currentPageIndex, index, totalPages, shouldLoad)}
position={position} position={position}
onLoad={onLoad} onLoad={onLoad}
onError={onError} onError={onError}