velocity based queue

This commit is contained in:
2025-11-17 01:01:59 +01:00
parent c56058b939
commit 42733a9a92
+83 -5
View File
@@ -46,6 +46,10 @@ interface RenderQueueRequest {
}
const RERENDER_DELTA = 48;
const MAX_PIXEL_RATIO = 2;
const FAST_SCROLL_VELOCITY_THRESHOLD = 0.8; // px/ms ~ 800px/s
const FAST_SCROLL_DWELL_THRESHOLD_MS = 120;
const SCROLL_VELOCITY_MIN_DELTA = 0.05;
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
const viewportStyle = window.getComputedStyle(viewportNode);
@@ -87,6 +91,7 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const [pixelRatio, setPixelRatio] = useState(1);
const [viewMode, setViewMode] = useState<ViewMode>('contain');
const [visiblePages, setVisiblePages] = useState<Set<number>>(new Set());
const [scrollVelocity, setScrollVelocity] = useState(0);
const pdfRef = useRef<PDFDocumentProxy | null>(null);
const wasmUrlRef = useRef<string | null>(null);
const pageNodeMapRef = useRef<Map<number, HTMLElement>>(new Map());
@@ -94,6 +99,12 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const renderQueueRef = useRef<RenderQueueRequest[]>([]);
const activeRendersRef = useRef(new Set<number>());
const maxConcurrentRendersRef = useRef(2);
const scrollVelocityRef = useRef({
lastPosition: 0,
lastTime: 0,
velocity: 0,
});
const scrollElementRef = useRef<HTMLElement | null>(null);
const requestQueueFlush = useCallback(() => {
const queue = renderQueueRef.current;
@@ -277,7 +288,7 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
return;
}
const ratio = window.devicePixelRatio || 1;
const ratio = Math.min(window.devicePixelRatio || 1, MAX_PIXEL_RATIO);
const descriptors: PageDescriptor[] = [];
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
if (cancelled) {
@@ -401,6 +412,20 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const intersectionRoot = viewportRef?.current || null;
const updateScrollVelocityState = useCallback((nextVelocity: number) => {
setScrollVelocity((previous) => {
const wasFast = Math.abs(previous) > FAST_SCROLL_VELOCITY_THRESHOLD;
const isFast = Math.abs(nextVelocity) > FAST_SCROLL_VELOCITY_THRESHOLD;
if (!isFast && !wasFast && Math.abs(previous - nextVelocity) < SCROLL_VELOCITY_MIN_DELTA) {
return previous;
}
if (Math.abs(previous - nextVelocity) < SCROLL_VELOCITY_MIN_DELTA && wasFast === isFast) {
return previous;
}
return nextVelocity;
});
}, []);
const registerPageNode = useCallback((pageNumber: number, node: HTMLElement | null) => {
const map = pageNodeMapRef.current;
const existing = map.get(pageNumber);
@@ -421,6 +446,8 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
useEffect(() => {
let frame: number | null = null;
let observer: IntersectionObserver | null = null;
let scrollElement: HTMLElement | null = null;
let scrollListener: (() => void) | null = null;
const attachObserver = () => {
const stackElement = containerRef.current;
@@ -431,6 +458,34 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const root = intersectionRoot
|| stackElement.closest('.document-viewer__viewport')
|| undefined;
scrollElement = (viewportRef?.current || stackElement.closest('.document-viewer__viewport')) ?? null;
scrollElementRef.current = scrollElement;
if (scrollElement && !scrollListener) {
scrollVelocityRef.current = {
lastPosition: scrollElement.scrollTop,
lastTime: performance.now(),
velocity: 0,
};
scrollListener = () => {
if (!scrollElement) {
return;
}
const now = performance.now();
const position = scrollElement.scrollTop;
const elapsed = now - scrollVelocityRef.current.lastTime;
if (elapsed <= 0) {
return;
}
const velocity = (position - scrollVelocityRef.current.lastPosition) / elapsed;
scrollVelocityRef.current = {
lastPosition: position,
lastTime: now,
velocity,
};
updateScrollVelocityState(velocity);
};
scrollElement.addEventListener('scroll', scrollListener, { passive: true });
}
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const attr = entry.target.getAttribute('data-page-number');
@@ -462,8 +517,14 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
if (intersectionObserverRef.current === observer) {
intersectionObserverRef.current = null;
}
if (scrollElement && scrollListener) {
scrollElement.removeEventListener('scroll', scrollListener);
}
if (scrollElementRef.current === scrollElement) {
scrollElementRef.current = null;
}
};
}, [handlePageVisibilityChange, intersectionRoot]);
}, [handlePageVisibilityChange, intersectionRoot, updateScrollVelocityState, viewportRef]);
const pageElements = useMemo(() => (
pages.map((page) => (
@@ -481,11 +542,13 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
enqueueRender={enqueueRender}
cancelRenderRequest={cancelRenderRequest}
onRenderFinished={handleRenderFinished}
scrollVelocity={scrollVelocity}
/>
</Fragment>
))
), [cancelRenderRequest, enqueueRender, handlePageClick, handleRenderFinished,
pages, pixelRatio, registerPageNode, shouldRenderPage, viewportHeight, viewportWidth, viewMode]);
pages, pixelRatio, registerPageNode, scrollVelocity, shouldRenderPage, viewportHeight, viewportWidth,
viewMode]);
return (
<div className={viewerClasses}>
@@ -522,6 +585,7 @@ interface PdfPageCanvasProps {
enqueueRender: (request: RenderQueueRequest) => void;
cancelRenderRequest: (pageNumber: number) => void;
onRenderFinished: (pageNumber: number) => void;
scrollVelocity: number;
}
function PdfPageCanvas({
@@ -537,6 +601,7 @@ function PdfPageCanvas({
enqueueRender,
cancelRenderRequest,
onRenderFinished,
scrollVelocity,
}: PdfPageCanvasProps): JSX.Element {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -609,6 +674,19 @@ function PdfPageCanvas({
}
}, [descriptor.number, descriptor.scale, onRenderFinished, pdfRef, pixelRatio]);
const meetsVelocityRequirement = useMemo(() => {
if (!shouldRender) {
return true;
}
const absoluteVelocity = Math.abs(scrollVelocity);
if (absoluteVelocity <= FAST_SCROLL_VELOCITY_THRESHOLD) {
return true;
}
const estimatedMs = (descriptor.height + viewportHeight)
/ Math.max(absoluteVelocity, 0.001);
return estimatedMs >= FAST_SCROLL_DWELL_THRESHOLD_MS;
}, [descriptor.height, scrollVelocity, shouldRender, viewportHeight]);
useEffect(() => {
if (!canvasMounted) {
return undefined;
@@ -635,7 +713,7 @@ function PdfPageCanvas({
}, [descriptor.number, resetCanvas]);
useEffect(() => {
if (!shouldRender) {
if (!shouldRender || !meetsVelocityRequirement) {
setAllowRender(false);
cancelRenderRequest(descriptor.number);
return;
@@ -660,7 +738,7 @@ function PdfPageCanvas({
setAllowRender(false);
cancelRenderRequest(descriptor.number);
};
}, [cancelRenderRequest, descriptor.number, enqueueRender, shouldRender]);
}, [cancelRenderRequest, descriptor.number, enqueueRender, meetsVelocityRequirement, shouldRender]);
useEffect(() => {
if (allowRender) {