feat: preload the whole current chapter in continuous mode
Eager-load every page of the current chapter rather than a bounded rolling window, so pages are fetched up front (the browser paces them over its per-origin connection limit, top-to-bottom = reading order) instead of filling in as they're scrolled onto. This drops the scrollLeadIdx/eagerThrough machinery in favour of a plain `loading="eager"`. The first few pages of the *next* chapter are already warmed when the reader nears the end of the current one (the next-chapter preloader), so flipping over stays instant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,13 +170,10 @@ test.describe('reader ?page=N deep link', () => {
|
||||
|
||||
await expect(page.getByTestId('reader-continuous')).toBeVisible();
|
||||
|
||||
// Pages 1..=4 (1-indexed in the testid, 0-indexed in the
|
||||
// template; initialIndex = 3 means we eager-load at least 0..=3,
|
||||
// i.e. testids 1..4). Without this guard, page 2+ would be lazy
|
||||
// and their 0×0 placeholders would let the scroll target
|
||||
// appear far above its final position. The rolling preload window
|
||||
// (PRELOAD_AHEAD) extends the eager set a few pages past the
|
||||
// target, so on this 6-page chapter every page is eager.
|
||||
// The whole chapter is eager, so the scroll target (page 4) and
|
||||
// every page before it have settled heights before the
|
||||
// scroll-to-`?page=N` effect runs — the target can't be pushed
|
||||
// past the viewport by later pages loading in.
|
||||
for (const n of [1, 2, 3, 4, 5, 6]) {
|
||||
await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute(
|
||||
'loading',
|
||||
@@ -185,28 +182,21 @@ test.describe('reader ?page=N deep link', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('continuous mode: no ?page= eager-loads a leading preload window, not the whole chapter', async ({
|
||||
test('continuous mode: eager-loads the whole chapter up front', async ({
|
||||
page
|
||||
}) => {
|
||||
await mockReader(page, 'continuous');
|
||||
await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);
|
||||
|
||||
await expect(page.getByTestId('reader-continuous')).toBeVisible();
|
||||
// initialIndex = 0; the rolling window (PRELOAD_AHEAD = 3) eager-loads
|
||||
// the first few pages ahead of the reader so they don't pop in on
|
||||
// scroll ...
|
||||
for (const n of [1, 2, 3]) {
|
||||
// Every page is eager — the whole current chapter is preloaded so
|
||||
// pages don't pop in / reflow as the reader scrolls onto them.
|
||||
for (const n of [1, 2, 3, 4, 5, 6]) {
|
||||
await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute(
|
||||
'loading',
|
||||
'eager'
|
||||
);
|
||||
}
|
||||
// ... while the tail stays lazy so a long chapter doesn't fetch every
|
||||
// page at once on open.
|
||||
await expect(page.getByTestId('reader-page-6')).toHaveAttribute(
|
||||
'loading',
|
||||
'lazy'
|
||||
);
|
||||
});
|
||||
|
||||
test('single mode: ?page=N opens at the requested page', async ({ page }) => {
|
||||
|
||||
@@ -55,20 +55,22 @@ async function goContinuous(page: Page, path: string) {
|
||||
await expect(page.getByTestId('reader-continuous')).toBeVisible();
|
||||
}
|
||||
|
||||
test('opening at page 1 eager-loads the leading window but leaves far pages lazy', async ({ page }) => {
|
||||
test('eager-loads the whole current chapter up front, not just the visible pages', async ({ page }) => {
|
||||
await mockReader(page);
|
||||
await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}`);
|
||||
|
||||
// The first page is always eager (it's on screen).
|
||||
await expect(page.getByTestId('reader-page-1')).toHaveAttribute('loading', 'eager');
|
||||
// A page far past the leading window is not fetched up front.
|
||||
await expect(page.getByTestId('reader-page-10')).toHaveAttribute('loading', 'lazy');
|
||||
});
|
||||
// Every page — including ones far below the fold — is eager, so the
|
||||
// browser fetches the whole chapter up front instead of on scroll.
|
||||
for (let n = 1; n <= PAGE_COUNT; n++) {
|
||||
await expect(page.getByTestId(`reader-page-${n}`)).toHaveAttribute('loading', 'eager');
|
||||
}
|
||||
|
||||
test('opening deep in the chapter eager-loads the pages around that position', async ({ page }) => {
|
||||
await mockReader(page);
|
||||
// Deep-link to page 8: the window now covers the tail, so page 10 is eager.
|
||||
await goContinuous(page, `/manga/${MANGA_ID}/chapter/${CH1}?page=8`);
|
||||
|
||||
await expect(page.getByTestId('reader-page-10')).toHaveAttribute('loading', 'eager');
|
||||
// ... and a page well below the fold actually finishes loading without
|
||||
// ever scrolling to it — the observable payoff of preloading.
|
||||
const lastPage = page.getByTestId(`reader-page-${PAGE_COUNT}`);
|
||||
await expect(async () => {
|
||||
const loaded = await lastPage.evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0);
|
||||
expect(loaded).toBe(true);
|
||||
}).toPass();
|
||||
expect(await page.evaluate(() => window.scrollY)).toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user