diff --git a/src/app/manga/read/page.tsx b/src/app/manga/read/page.tsx index 3658bb7..5fb4447 100644 --- a/src/app/manga/read/page.tsx +++ b/src/app/manga/read/page.tsx @@ -7,6 +7,7 @@ import { ArrowDownWideNarrow, ArrowUpWideNarrow } from 'lucide-react'; import { getAllMangaReadRecords, saveMangaReadRecord } from '@/lib/db.client'; import type { MangaChapter, MangaDetail, MangaReadRecord } from '@/lib/manga.types'; +import { processImageUrl } from '@/lib/utils'; import ProxyImage from '@/components/ProxyImage'; @@ -17,6 +18,7 @@ const READ_MODE_STORAGE_KEY = 'mangaReadMode'; const SCALE_MODE_STORAGE_KEY = 'mangaScaleMode'; const PAGE_GAP_STORAGE_KEY = 'mangaPageGap'; const SAVE_INTERVAL_MS = 10000; +const PRELOAD_PAGE_COUNT = 5; const READ_MODE_OPTIONS: Array<{ value: ReadMode; label: string }> = [ { value: 'single', label: '单页' }, @@ -98,6 +100,7 @@ export default function MangaReadPage() { const previousReadModeRef = useRef('vertical'); const requestVerticalPageSyncRef = useRef<(() => void) | null>(null); const currentVerticalPageIndexRef = useRef(0); + const preloadedImageUrlsRef = useRef>(new Set()); const getCurrentVerticalPageIndex = () => { if (!verticalPageRefs.current.length) return 0; @@ -121,6 +124,17 @@ export default function MangaReadPage() { requestVerticalPageSyncRef.current?.(); }; + const getPreloadAnchorPage = () => ( + readMode === 'vertical' ? currentVerticalPageIndexRef.current : activePage + ); + + const getImageLoadingStrategy = (index: number): 'eager' | 'lazy' => { + const anchorPage = getPreloadAnchorPage(); + return index >= Math.max(anchorPage - 1, 0) && index <= anchorPage + PRELOAD_PAGE_COUNT + ? 'eager' + : 'lazy'; + }; + useEffect(() => { if (typeof window === 'undefined') return; const savedMode = window.localStorage.getItem(READ_MODE_STORAGE_KEY) as ReadMode | null; @@ -206,6 +220,7 @@ export default function MangaReadPage() { useEffect(() => { setActivePage(0); restoredChapterKeyRef.current = null; + preloadedImageUrlsRef.current.clear(); }, [chapterId]); useEffect(() => { @@ -393,6 +408,23 @@ export default function MangaReadPage() { pendingRecordDirtyRef.current = true; }, [activePage, chapterId, chapterName, cover, mangaId, pages.length, readMode, sourceId, sourceName, title]); + useEffect(() => { + if (typeof window === 'undefined' || !pages.length) return; + + const anchorPage = getPreloadAnchorPage(); + const preloadTargets = pages.slice(anchorPage + 1, anchorPage + 1 + PRELOAD_PAGE_COUNT); + + preloadTargets.forEach((page) => { + const resolvedUrl = processImageUrl(page); + if (!resolvedUrl || preloadedImageUrlsRef.current.has(resolvedUrl)) return; + + const img = new window.Image(); + img.decoding = 'async'; + img.src = resolvedUrl; + preloadedImageUrlsRef.current.add(resolvedUrl); + }); + }, [activePage, pages, readMode]); + useEffect(() => { if (!mangaId || !sourceId || !chapterId) return; @@ -576,12 +608,12 @@ export default function MangaReadPage() { const imageClassName = useMemo(() => { if (scaleMode === 'original') { - return 'mx-auto h-auto w-auto max-w-none object-none'; + return 'block mx-auto h-auto w-auto max-w-none object-none'; } if (readMode === 'single' || readMode === 'double') { - return 'mx-auto max-h-[calc(100vh-8rem)] w-auto max-w-full object-contain'; + return 'block h-auto w-full object-contain sm:mx-auto sm:max-h-[calc(100vh-8rem)] sm:w-auto sm:max-w-full'; } - return 'h-auto w-full object-contain'; + return 'block h-auto w-full object-contain'; }, [readMode, scaleMode]); const handleReaderClick = (event: MouseEvent) => { @@ -742,7 +774,7 @@ export default function MangaReadPage() { )}
{showChapterComplete && ( @@ -812,6 +844,7 @@ export default function MangaReadPage() { originalSrc={page} alt={`${chapterName}-${index + 1}`} className={imageClassName} + loading={getImageLoadingStrategy(index)} onLoad={handleVerticalImageLoad} />
@@ -824,9 +857,14 @@ export default function MangaReadPage() { style={{ gap: `${pageGap}px` }} > {pages.map((page, index) => ( -
+
- +
))} @@ -843,6 +881,7 @@ export default function MangaReadPage() { originalSrc={page} alt={`${chapterName}-${activePage + index + 1}`} className={imageClassName} + loading='eager' />
))} diff --git a/src/components/ProxyImage.tsx b/src/components/ProxyImage.tsx index 5684344..251c766 100644 --- a/src/components/ProxyImage.tsx +++ b/src/components/ProxyImage.tsx @@ -16,6 +16,8 @@ const ProxyImage: React.FC = ({ displaySrc, retryDelay = 2000, retryOnError = true, + loading = 'lazy', + decoding = 'async', onError, src: _src, ...props @@ -41,6 +43,8 @@ const ProxyImage: React.FC = ({ ); diff --git a/src/components/manga/MangaLayout.tsx b/src/components/manga/MangaLayout.tsx index f5bd42d..d257fff 100644 --- a/src/components/manga/MangaLayout.tsx +++ b/src/components/manga/MangaLayout.tsx @@ -151,8 +151,10 @@ export default function MangaLayout({ children }: MangaLayoutProps) {
{children}