perf: implement background thumbnail generation and inter-post preloading
Key changes: - Added Web Worker for background image thumbnailing with a 1MiB threshold to optimize CPU/memory usage. - Implemented a serial task queue for memory-safe high-res image processing, preventing OOM crashes. - Added inter-post preloading in the modal for seamless 'Previous/Next' navigation. - Refined scanning UI with double-buffering and a dark background to completely eliminate white flashes. - Renamed project to 'instaarchive-viewer' in package.json. - Fixed 'Open image in new tab' by denylisting /archives and /api in PWA config.
This commit is contained in:
@@ -195,8 +195,8 @@ export const useArchiveScanner = (
|
||||
const type = isVideo(matchedFile.name) ? 'video' : 'image';
|
||||
const url = matchedFile.url || URL.createObjectURL(new Blob([await matchedFile.arrayBuffer()], { type: type === 'video' ? 'video/mp4' : 'image/jpeg' }));
|
||||
const existingMedia = post.media!.find(media => media.index === mIdx + 1);
|
||||
if (existingMedia) { if (type === 'video' && existingMedia.type === 'image') post.media = post.media!.map(media => media.index === mIdx + 1 ? { name: matchedFile!.name, url, type, index: mIdx + 1 } : media); }
|
||||
else post.media!.push({ name: matchedFile.name, url, type, index: mIdx + 1 });
|
||||
if (existingMedia) { if (type === 'video' && existingMedia.type === 'image') post.media = post.media!.map(media => media.index === mIdx + 1 ? { name: matchedFile!.name, url, type, index: mIdx + 1, size: matchedFile!.size } : media); }
|
||||
else post.media!.push({ name: matchedFile.name, url, type, index: mIdx + 1, size: matchedFile.size });
|
||||
}
|
||||
}
|
||||
if (post.media!.length > 0) postsMap.set(postId, post);
|
||||
@@ -251,8 +251,8 @@ export const useArchiveScanner = (
|
||||
const url = file.url || URL.createObjectURL(new Blob([await file.arrayBuffer()], { type: type === 'video' ? 'video/mp4' : 'image/jpeg' }));
|
||||
if (type === 'image') throttledSetScanningImage(url);
|
||||
const existingMedia = post.media!.find(m => m.index === index);
|
||||
if (existingMedia) { if (type === 'video' && existingMedia.type === 'image') post.media = post.media!.map(m => m.index === index ? { name: file.name, url, type, index } : m); }
|
||||
else post.media!.push({ name: file.name, url, type, index });
|
||||
if (existingMedia) { if (type === 'video' && existingMedia.type === 'image') post.media = post.media!.map(m => m.index === index ? { name: file.name, url, type, index, size: file.size } : m); }
|
||||
else post.media!.push({ name: file.name, url, type, index, size: file.size });
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
@@ -297,7 +297,7 @@ export const useArchiveScanner = (
|
||||
const type = isVideo(file.name) ? 'video' : 'image';
|
||||
const url = file.url || URL.createObjectURL(new Blob([await file.arrayBuffer()], { type: type === 'video' ? 'video/mp4' : 'image/jpeg' }));
|
||||
if (type === 'image') throttledSetScanningImage(url);
|
||||
post.media.push({ name: file.name, url, type, index: idx + 1 });
|
||||
post.media.push({ name: file.name, url, type, index: idx + 1, size: file.size });
|
||||
}
|
||||
post.thumbnail = post.media[0].url;
|
||||
postsMap.set(postId, post);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import * as idb from 'idb-keyval';
|
||||
|
||||
interface ThumbnailRequest {
|
||||
id: string;
|
||||
url: string;
|
||||
blob?: Blob;
|
||||
}
|
||||
|
||||
const THUMBNAIL_WIDTH = 400;
|
||||
|
||||
export const useThumbnailQueue = () => {
|
||||
const [cacheHits, setCacheHits] = useState<Map<string, string>>(new Map());
|
||||
const queueRef = useRef<ThumbnailRequest[]>([]);
|
||||
const isProcessingRef = useRef(false);
|
||||
const workerRef = useRef<Worker | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize worker with relative URL (vite will handle this)
|
||||
workerRef.current = new Worker(new URL('../lib/thumbnail-worker.ts', import.meta.url), {
|
||||
type: 'module'
|
||||
});
|
||||
|
||||
workerRef.current.onmessage = async (e) => {
|
||||
const { id, blob, error } = e.data;
|
||||
|
||||
if (!error && blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
setCacheHits(prev => new Map(prev).set(id, url));
|
||||
|
||||
// Persist to IndexedDB (as dataURL for simple retrieval or keep as Blob)
|
||||
try {
|
||||
await idb.set(`thumb_${id}`, blob);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// Process next in queue
|
||||
isProcessingRef.current = false;
|
||||
processNext();
|
||||
};
|
||||
|
||||
return () => {
|
||||
workerRef.current?.terminate();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const processNext = useCallback(async () => {
|
||||
if (isProcessingRef.current || queueRef.current.length === 0 || !workerRef.current) return;
|
||||
|
||||
isProcessingRef.current = true;
|
||||
const request = queueRef.current.shift()!;
|
||||
|
||||
try {
|
||||
// 1. Double check cache before expensive work
|
||||
const cached = await idb.get(`thumb_${request.id}`);
|
||||
if (cached instanceof Blob) {
|
||||
const url = URL.createObjectURL(cached);
|
||||
setCacheHits(prev => new Map(prev).set(request.id, url));
|
||||
isProcessingRef.current = false;
|
||||
processNext();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Fetch original if no blob provided
|
||||
let blob = request.blob;
|
||||
if (!blob) {
|
||||
const res = await fetch(request.url);
|
||||
blob = await res.blob();
|
||||
}
|
||||
|
||||
// 3. Send to worker
|
||||
workerRef.current.postMessage({
|
||||
id: request.id,
|
||||
blob,
|
||||
width: THUMBNAIL_WIDTH
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[ThumbnailQueue] Failed to process:', request.id, err);
|
||||
isProcessingRef.current = false;
|
||||
processNext();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const requestThumbnail = useCallback(async (id: string, url: string, blob?: Blob) => {
|
||||
// 1. Sync check state
|
||||
if (cacheHits.has(id)) return;
|
||||
|
||||
// 2. Async check idb
|
||||
const cached = await idb.get(`thumb_${id}`);
|
||||
if (cached instanceof Blob) {
|
||||
setCacheHits(prev => new Map(prev).set(id, URL.createObjectURL(cached)));
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Add to queue if not already there
|
||||
if (!queueRef.current.some(r => r.id === id)) {
|
||||
queueRef.current.push({ id, url, blob });
|
||||
processNext();
|
||||
}
|
||||
}, [cacheHits, processNext]);
|
||||
|
||||
return {
|
||||
cacheHits,
|
||||
requestThumbnail
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user