Fix webtoon pages never loading: drop IntersectionObserver for scroll-driven loading

IO with a shadow-host root never delivered intersections in Chromium
(verified live: pages stayed blank while the section.load→fetch→img
pipeline itself worked fine when probed from outside). Loading is now
driven from the rAF-throttled scroll handler that already tracked the
reading position: a window of [index-2, index+4] loads around the
viewport-center page, with a 'loading' guard against duplicate
in-flight loads. connectedCallback kicks the initial load window.
This commit is contained in:
2026-08-18 08:29:45 -04:00
parent ea268dffbe
commit e448d36641
+23 -19
View File
@@ -9,7 +9,7 @@ export class Webtoon extends HTMLElement {
#pages = []; #pages = [];
#index = -1; #index = -1;
#firstNav = true; #firstNav = true;
#loadIO = null; #connected = false;
#scrollRaf = null; #scrollRaf = null;
constructor() { constructor() {
@@ -57,28 +57,24 @@ export class Webtoon extends HTMLElement {
el.dataset.index = i; el.dataset.index = i;
el.style.minHeight = `${minH}px`; el.style.minHeight = `${minH}px`;
frag.append(el); frag.append(el);
return { el, section, loaded: false, img: null }; return { el, section, loaded: false, img: null, loading: false };
}); });
this.#root.append(frag); this.#root.append(frag);
// Loading is driven from scroll position (#updateIndex), not an
this.#loadIO = new IntersectionObserver( // IntersectionObserver: IO with a shadow-host root proved unreliable
(entries) => { // in Chromium and would leave pages permanently blank.
for (const entry of entries) { if (this.#connected) this.#updateIndex();
if (!entry.isIntersecting) continue;
this.#loadIO.unobserve(entry.target);
this.#loadPage(Number(entry.target.dataset.index));
} }
},
// generous margin so pages are ready before they scroll into view connectedCallback() {
{ root: this, rootMargin: "150% 0%" }, this.#connected = true;
); if (this.#pages.length) this.#updateIndex();
for (const p of this.#pages) this.#loadIO.observe(p.el);
} }
async #loadPage(i) { async #loadPage(i) {
const p = this.#pages[i]; const p = this.#pages[i];
if (!p || p.loaded) return; if (!p || p.loaded || p.loading) return;
p.loaded = true; p.loading = true;
try { try {
const url = await p.section.load?.(); const url = await p.section.load?.();
if (!url) return; if (!url) return;
@@ -97,12 +93,21 @@ export class Webtoon extends HTMLElement {
p.el.style.minHeight = "0"; p.el.style.minHeight = "0";
p.el.append(img); p.el.append(img);
p.img = img; p.img = img;
p.loaded = true;
} catch (e) { } catch (e) {
p.loaded = false;
console.warn("webtoon: failed to load page", i + 1, e); console.warn("webtoon: failed to load page", i + 1, e);
} finally {
p.loading = false;
} }
} }
// Load a window around the reading position (a few pages of lookahead).
#loadAround(idx) {
const start = Math.max(0, idx - 2);
const end = Math.min(this.#pages.length - 1, idx + 4);
for (let i = start; i <= end; i++) this.#loadPage(i);
}
// Free far-away pages (images + their blob URLs) while keeping the // Free far-away pages (images + their blob URLs) while keeping the
// aspect-ratio placeholder, so the column height stays stable. // aspect-ratio placeholder, so the column height stays stable.
#unloadFar(current) { #unloadFar(current) {
@@ -114,7 +119,6 @@ export class Webtoon extends HTMLElement {
p.img = null; p.img = null;
p.loaded = false; p.loaded = false;
p.section.unload?.(); p.section.unload?.();
this.#loadIO.observe(p.el);
} }
} }
} }
@@ -152,6 +156,7 @@ export class Webtoon extends HTMLElement {
this.#index = idx; this.#index = idx;
this.#unloadFar(idx); this.#unloadFar(idx);
} }
this.#loadAround(idx);
this.#report(fraction); this.#report(fraction);
} }
@@ -217,7 +222,6 @@ export class Webtoon extends HTMLElement {
} }
destroy() { destroy() {
this.#loadIO?.disconnect();
} }
} }