perf(showcase): prefetch adjacent examples and shrink gallery thumbnails

- Warm the HTTP cache for examples adjacent to the active one after
  expand and on every navigation, so prev/next feels instant (images
  only, deduped, low fetch priority)
- Add GALLERY_THUMBNAIL optimization mode (width=160) for the 72px
  gallery strip instead of reusing the 450px card thumbnails
- Hint priorities: fetchpriority=high on the main media, low on
  strip thumbnails
This commit is contained in:
Will Miao
2026-09-01 22:26:50 +08:00
parent 9584fa85c9
commit ed2a17970f
5 changed files with 153 additions and 6 deletions
@@ -197,4 +197,64 @@ describe('Showcase gallery', () => {
expect(document.querySelector('.gallery-indicator-bar')).toBeTruthy();
expect(document.querySelectorAll('.gallery-thumb')).toHaveLength(0);
});
it('prefetches adjacent example images (skipping videos) while expanded', async () => {
const { renderShowcaseContent, initShowcaseContent, updateMainDisplay } = await import(SHOWCASE_MODULE);
const prefetched = [];
class MockImage {
set src(value) { prefetched.push(value); }
set fetchPriority(_value) { /* jsdom lacks fetchPriority */ }
}
vi.stubGlobal('Image', MockImage);
// Unique URLs: the module-level prefetch dedup set persists across tests
const images = [
{ url: 'https://image.civitai.com/pf/aaa.jpeg', width: 100, height: 100, nsfwLevel: 0 },
{ url: 'https://image.civitai.com/pf/bbb.jpeg', width: 100, height: 100, nsfwLevel: 0 },
{ url: 'https://image.civitai.com/pf/ccc.mp4', width: 100, height: 100, nsfwLevel: 0 },
];
document.body.innerHTML = `<div id="showcase-tab">${renderShowcaseContent(images, [], PREVIEW_URL, true)}</div>`;
initShowcaseContent(document.querySelector('.showcase-gallery'));
// galleryState.activeIndex persists across tests → pin it to 0
updateMainDisplay(0);
// Active index 0 → prefetches index 1; index 2 is a video and is skipped
expect(prefetched).toContain('https://image.civitai.com/pf/bbb.jpeg');
expect(prefetched).not.toContain('https://image.civitai.com/pf/ccc.mp4');
// Navigating to 1 prefetches the new neighbor (index 0)
updateMainDisplay(1);
expect(prefetched).toContain('https://image.civitai.com/pf/aaa.jpeg');
// Navigating back does not duplicate prefetch requests
const count = prefetched.length;
updateMainDisplay(0);
expect(prefetched).toHaveLength(count);
vi.unstubAllGlobals();
});
it('does not prefetch while collapsed', async () => {
const { renderShowcaseContent, initShowcaseContent } = await import(SHOWCASE_MODULE);
const prefetched = [];
class MockImage {
set src(value) { prefetched.push(value); }
set fetchPriority(_value) { /* jsdom lacks fetchPriority */ }
}
vi.stubGlobal('Image', MockImage);
const images = [
{ url: 'https://image.civitai.com/pc/ddd.jpeg', width: 100, height: 100, nsfwLevel: 0 },
{ url: 'https://image.civitai.com/pc/eee.jpeg', width: 100, height: 100, nsfwLevel: 0 },
];
document.body.innerHTML = `<div id="showcase-tab">${renderShowcaseContent(images, [], PREVIEW_URL)}</div>`;
initShowcaseContent(document.querySelector('.showcase-gallery'));
expect(prefetched).toHaveLength(0);
vi.unstubAllGlobals();
});
});
+34
View File
@@ -9,6 +9,7 @@ import {
getOptimizedUrl,
getShowcaseUrl,
getThumbnailUrl,
getGalleryThumbnailUrl,
extractCivitaiImageId,
extractCivitaiModelUrlParts,
classifyModelRelinkUrl,
@@ -22,6 +23,7 @@ describe('civitaiUtils', () => {
it('should have correct mode values', () => {
expect(OptimizationMode.SHOWCASE).toBe('showcase');
expect(OptimizationMode.THUMBNAIL).toBe('thumbnail');
expect(OptimizationMode.GALLERY_THUMBNAIL).toBe('gallery-thumbnail');
});
});
@@ -107,6 +109,22 @@ describe('civitaiUtils', () => {
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450,optimized=true/12345.jpeg');
});
it('should rewrite image URLs with /original=true for gallery-thumbnail mode (width=160)', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.GALLERY_THUMBNAIL);
expect(wasRewritten).toBe(true);
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=160,optimized=true/12345.jpeg');
});
it('should rewrite video URLs with /original=true for gallery-thumbnail mode', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'video', OptimizationMode.GALLERY_THUMBNAIL);
expect(wasRewritten).toBe(true);
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=160,optimized=true/12345.mp4');
});
it('should not rewrite URLs without /original=true', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450/12345.jpeg';
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
@@ -232,6 +250,22 @@ describe('civitaiUtils', () => {
});
});
describe('getGalleryThumbnailUrl', () => {
it('should return gallery-thumbnail-optimized URL (width=160)', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
const thumbnailUrl = getGalleryThumbnailUrl(originalUrl, 'image');
expect(thumbnailUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=160,optimized=true/12345.jpeg');
});
it('should handle videos for gallery thumbnails', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
const thumbnailUrl = getGalleryThumbnailUrl(originalUrl, 'video');
expect(thumbnailUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=160,optimized=true/12345.mp4');
});
});
describe('isCivitaiUrl', () => {
it('should return true for CivitAI URLs', () => {
expect(isCivitaiUrl('https://image.civitai.com/something')).toBe(true);