From e448d36641e41f27f11a24dfdb2b17e2f276d1a9 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 18 Aug 2026 08:29:45 -0400 Subject: [PATCH] Fix webtoon pages never loading: drop IntersectionObserver for scroll-driven loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- webtoon.js | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/webtoon.js b/webtoon.js index f597e6d..7227ac7 100644 --- a/webtoon.js +++ b/webtoon.js @@ -9,7 +9,7 @@ export class Webtoon extends HTMLElement { #pages = []; #index = -1; #firstNav = true; - #loadIO = null; + #connected = false; #scrollRaf = null; constructor() { @@ -57,28 +57,24 @@ export class Webtoon extends HTMLElement { el.dataset.index = i; el.style.minHeight = `${minH}px`; frag.append(el); - return { el, section, loaded: false, img: null }; + return { el, section, loaded: false, img: null, loading: false }; }); this.#root.append(frag); + // Loading is driven from scroll position (#updateIndex), not an + // IntersectionObserver: IO with a shadow-host root proved unreliable + // in Chromium and would leave pages permanently blank. + if (this.#connected) this.#updateIndex(); + } - this.#loadIO = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - 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 - { root: this, rootMargin: "150% 0%" }, - ); - for (const p of this.#pages) this.#loadIO.observe(p.el); + connectedCallback() { + this.#connected = true; + if (this.#pages.length) this.#updateIndex(); } async #loadPage(i) { const p = this.#pages[i]; - if (!p || p.loaded) return; - p.loaded = true; + if (!p || p.loaded || p.loading) return; + p.loading = true; try { const url = await p.section.load?.(); if (!url) return; @@ -97,12 +93,21 @@ export class Webtoon extends HTMLElement { p.el.style.minHeight = "0"; p.el.append(img); p.img = img; + p.loaded = true; } catch (e) { - p.loaded = false; 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 // aspect-ratio placeholder, so the column height stays stable. #unloadFar(current) { @@ -114,7 +119,6 @@ export class Webtoon extends HTMLElement { p.img = null; p.loaded = false; p.section.unload?.(); - this.#loadIO.observe(p.el); } } } @@ -152,6 +156,7 @@ export class Webtoon extends HTMLElement { this.#index = idx; this.#unloadFar(idx); } + this.#loadAround(idx); this.#report(fraction); } @@ -217,7 +222,6 @@ export class Webtoon extends HTMLElement { } destroy() { - this.#loadIO?.disconnect(); } }