feat: preload the current chapter ahead of the reader
Continuous mode eager-loaded only pages 0..=initialIndex; everything below stayed `loading="lazy"`, so pages fetched and reflowed in as they scrolled into view. Now a rolling window eager-loads a few pages ahead of the reading position (PRELOAD_AHEAD) so they arrive — fetched and sized — while still below the fold, then swap in without pop-in. The window rolls forward off `scrollLeadIdx`, the top-of-viewport page measured from live rects and guarded by `scrollY`. It deliberately does not use the read-progress high-water mark: before images load they are 0-height and pile into the viewport, which would latch that mark to the last page and eager-load the whole chapter on open. Pages past the window stay lazy so opening a long chapter doesn't fetch every page at once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -971,6 +971,61 @@
|
||||
|
||||
const furthestPage = $derived(mode === 'single' ? index + 1 : progressPage);
|
||||
|
||||
// ---- Current-chapter rolling preload window (continuous mode) ----
|
||||
// Eager-load pages up to this index so they're fetched (and sized) a few
|
||||
// pages ahead of the reading position — they arrive while still below the
|
||||
// fold instead of popping in / reflowing as the user scrolls onto them.
|
||||
// Pages past the window stay lazy so opening a long chapter doesn't fire
|
||||
// every request at once.
|
||||
//
|
||||
// The window anchors on `initialIndex` (deterministic at open, and keeps
|
||||
// the scroll-settle guarantee that pages `0..=initialIndex` are eager) and
|
||||
// rolls forward by `scrollLeadIdx` — the page at the top of the viewport,
|
||||
// measured from live geometry below. It deliberately does NOT use the
|
||||
// read-progress high-water (`furthestPage`): before images load they're
|
||||
// 0-height and pile into the viewport, which latches that mark to the last
|
||||
// page and would eager-load the whole chapter on open.
|
||||
const PRELOAD_AHEAD = 3;
|
||||
let scrollLeadIdx = $state(0);
|
||||
const eagerThrough = $derived(
|
||||
Math.max(1, initialIndex, scrollLeadIdx) + PRELOAD_AHEAD
|
||||
);
|
||||
|
||||
// Track the top-of-viewport page from live rects so the window rolls with
|
||||
// actual scrolling. Guarded by `scrollY` so the pre-load 0-height pile-up
|
||||
// (all rects collapsed near the top) can't inflate the lead at open.
|
||||
$effect(() => {
|
||||
if (!browser || mode !== 'continuous') return;
|
||||
const measure = () => {
|
||||
if (window.scrollY < 4) {
|
||||
scrollLeadIdx = 0;
|
||||
return;
|
||||
}
|
||||
const els = continuousPageEls;
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i] && els[i].getBoundingClientRect().bottom > 1) {
|
||||
scrollLeadIdx = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
scrollLeadIdx = Math.max(0, els.length - 1);
|
||||
};
|
||||
measure();
|
||||
let raf = 0;
|
||||
const onScroll = () => {
|
||||
if (raf) return;
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = 0;
|
||||
measure();
|
||||
});
|
||||
};
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
if (raf) cancelAnimationFrame(raf);
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const nc = nextChapter;
|
||||
// Drop a previous chapter's warmed images once the next chapter
|
||||
@@ -1308,12 +1363,15 @@
|
||||
load scroll-to-`?page=N` effect fires. Without this,
|
||||
`scrollIntoView(target)` lands while prior pages are
|
||||
still 0×0 placeholders and the target gets pushed
|
||||
past the viewport as they load. -->
|
||||
past the viewport as they load. `eagerThrough` widens
|
||||
that set into a rolling window a few pages ahead of the
|
||||
reading position so pages load before they're scrolled
|
||||
onto rather than filling in underneath the viewport. -->
|
||||
<img
|
||||
src={fileUrl(p.storage_key)}
|
||||
alt={`${manga.title} chapter ${chapter.number} page ${i + 1}`}
|
||||
class="page-image"
|
||||
loading={i <= Math.max(1, initialIndex) ? 'eager' : 'lazy'}
|
||||
loading={i <= eagerThrough ? 'eager' : 'lazy'}
|
||||
oncontextmenu={(e) => onPageContextMenu(e, p.id)}
|
||||
onpointerdown={(e) => onPagePointerDown(e, p.id)}
|
||||
onpointermove={onPagePointerMove}
|
||||
|
||||
Reference in New Issue
Block a user