IntersectionObserver 후킹으로 백그라운드 탭 이미지 로딩 문제 해결

- @run-at document-start로 변경하여 페이지 스크립트보다 먼저 실행
- IntersectionObserver.observe() 후킹: observe() 즉시 콜백 실행하도록 하여
  백그라운드 탭에서도 IntersectionObserver가 동작하게 함
- 기존 로직은 DOMContentLoaded 후 실행되도록 main() 함수로 분리
- scrollHeight 동적 대응 로직 유지
This commit is contained in:
user01
2026-06-14 03:31:09 +09:00
parent 36d41457f5
commit c6bc10fbeb

View File

@@ -13,13 +13,48 @@
// @grant GM_download // @grant GM_download
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.js
// @run-at document-end // @run-at document-start
// @license MIT // @license MIT
// ==/UserScript== // ==/UserScript==
(function () { (function () {
'use strict'; 'use strict';
// ---------------------------------------------------------------
// Phase 1: IntersectionObserver 후킹 (document-start 에서 즉시 실행)
// 백그라운드 탭에서 IO가 동작하지 않아도 observe() 즉시 콜백 실행
// ---------------------------------------------------------------
const OrigIO = window.IntersectionObserver;
if (OrigIO) {
const HookedIO = function (callback, options) {
const instance = new OrigIO(callback, options);
const origObserve = instance.observe.bind(instance);
instance.observe = function (target) {
origObserve(target);
setTimeout(() => {
try {
callback([{
target: target,
isIntersecting: true,
intersectionRatio: 1,
boundingClientRect: target.getBoundingClientRect(),
intersectionRect: target.getBoundingClientRect(),
rootBounds: null,
time: performance.now()
}], instance);
} catch (_) { }
}, 50);
};
return instance;
};
HookedIO.prototype = OrigIO.prototype;
window.IntersectionObserver = HookedIO;
}
// ---------------------------------------------------------------
// Phase 2: DOM 로딩 후 나머지 로직
// ---------------------------------------------------------------
const main = () => {
// 사이트가 console을 모니터링/차단하므로 자체 로거 사용 // 사이트가 console을 모니터링/차단하므로 자체 로거 사용
const log = (...args) => { const log = (...args) => {
try { console.log(...args); } catch (_) {} try { console.log(...args); } catch (_) {}
@@ -98,7 +133,7 @@
} }
const comicInfo = `<?xml version="1.0" encoding="utf-8"?> const comicInfo = `<?xml version="1.0" encoding="utf-8"?>
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>${title}</Title> <Title>${title}</Title>
<Series>${series}</Series> <Series>${series}</Series>
<Number>${number}</Number> <Number>${number}</Number>
@@ -124,8 +159,8 @@
<Format></Format> <Format></Format>
<BlackAndWhite>Unknown</BlackAndWhite> <BlackAndWhite>Unknown</BlackAndWhite>
<Manga>Yes</Manga> <Manga>Yes</Manga>
${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''} ${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
</ComicInfo>`; </ComicInfo>`;
return comicInfo; return comicInfo;
} }
@@ -187,17 +222,23 @@ ${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
return { isListPage, isEpisodePage }; return { isListPage, isEpisodePage };
} }
// lazy loading 트리거: data-src 복사 + loading=eager + 스크롤 // 이미지의 data-* 속성을 src로 강제 복사
const triggerLazyLoading = async () => { const forceLoadImages = (root) => {
const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document; const container = root || document;
const imgs = viewer.querySelectorAll('img'); container.querySelectorAll('img').forEach(img => {
imgs.forEach(img => {
img.setAttribute('loading', 'eager'); img.setAttribute('loading', 'eager');
['data-src', 'data-original', 'data-lazy-src', 'data-lazy'].forEach(attr => { ['data-src', 'data-original', 'data-lazy-src', 'data-lazy', 'data-srcset'].forEach(attr => {
const val = img.getAttribute(attr); const val = img.getAttribute(attr);
if (val && !img.src) img.src = val; if (val && !img.src) img.src = val;
}); });
}); });
};
// lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
const triggerLazyLoading = async () => {
const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document;
// 우선 현재 img들 강제 로딩
forceLoadImages(viewer);
// 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가 // 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가
let prevHeight = 0; let prevHeight = 0;
let stuckCount = 0; let stuckCount = 0;
@@ -219,9 +260,12 @@ ${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
await sleep(300); await sleep(300);
} }
window.scrollTo(0, maxScroll); window.scrollTo(0, maxScroll);
// 스크롤 후 추가된 img들도 강제 로딩 (백그라운드 탭 IO 대체)
forceLoadImages(viewer);
await sleep(1500); await sleep(1500);
} }
// 끝까지 다 내려간 상태에서 여유 대기 // 끝까지 다 내려간 상태에서 여유 대기 + 최종 강제 로딩
forceLoadImages(viewer);
await sleep(2000); await sleep(2000);
window.scrollTo(0, 0); window.scrollTo(0, 0);
await sleep(1000); await sleep(1000);
@@ -403,4 +447,12 @@ ${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
window.addEventListener('popstate', () => setTimeout(registerMenu, 500)); window.addEventListener('popstate', () => setTimeout(registerMenu, 500));
log('sbxh6Downloader loaded'); log('sbxh6Downloader loaded');
};
// DOM 로딩 완료 후 main() 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})(); })();