perf(showcase): cap main viewer image width at 2400 via display mode

- New OptimizationMode.DISPLAY (width=2400 for images, full quality for
  videos) and getDisplayUrl(); the in-modal main viewer renders at most
  ~1200 CSS px wide, so full-size originals wasted 50-70% bandwidth
- Main viewer and adjacent prefetch use display URLs; the full-size
  media viewer keeps using getShowcaseUrl for original quality
This commit is contained in:
Will Miao
2026-09-01 22:45:00 +08:00
parent 3005d2877e
commit f7b247f9e8
3 changed files with 63 additions and 5 deletions
@@ -22,7 +22,7 @@ import {
} from './MediaUtils.js'; } from './MediaUtils.js';
import { generateMetadataPanel } from './MetadataPanel.js'; import { generateMetadataPanel } from './MetadataPanel.js';
import { generateImageWrapper, generateVideoWrapper } from './MediaRenderers.js'; import { generateImageWrapper, generateVideoWrapper } from './MediaRenderers.js';
import { getShowcaseUrl, getGalleryThumbnailUrl } from '../../../utils/civitaiUtils.js'; import { getShowcaseUrl, getDisplayUrl, getGalleryThumbnailUrl } from '../../../utils/civitaiUtils.js';
import { openMediaViewer } from '../MediaViewer.js'; import { openMediaViewer } from '../MediaViewer.js';
import { escapeAttribute } from '../utils.js'; import { escapeAttribute } from '../utils.js';
@@ -311,8 +311,9 @@ function renderMediaItem(img, index, exampleFiles) {
originalRemoteUrl.endsWith('.mp4') || originalRemoteUrl.endsWith('.webm'); originalRemoteUrl.endsWith('.mp4') || originalRemoteUrl.endsWith('.webm');
const mediaType = isVideo ? 'video' : 'image'; const mediaType = isVideo ? 'video' : 'image';
// Optimize CivitAI URLs for showcase display (full quality) // Optimize CivitAI URLs for in-modal display (images capped at width=2400;
const remoteUrl = getShowcaseUrl(originalRemoteUrl, mediaType); // the full-size media viewer uses getShowcaseUrl separately)
const remoteUrl = getDisplayUrl(originalRemoteUrl, mediaType);
const localUrl = localFile ? localFile.path : ''; const localUrl = localFile ? localFile.path : '';
@@ -466,7 +467,9 @@ function prefetchAdjacentMedia() {
const isVideo = img.url.endsWith('.mp4') || img.url.endsWith('.webm'); const isVideo = img.url.endsWith('.mp4') || img.url.endsWith('.webm');
if (isVideo) return; if (isVideo) return;
const url = getShowcaseUrl(img.url, 'image'); // Must match the main viewer's URL (display mode) or the warmed
// cache entry is never used
const url = getDisplayUrl(img.url, 'image');
if (prefetchedUrls.has(url)) return; if (prefetchedUrls.has(url)) return;
prefetchedUrls.add(url); prefetchedUrls.add(url);
+22 -1
View File
@@ -9,6 +9,9 @@
export const OptimizationMode = { export const OptimizationMode = {
/** Full quality for showcase/display - uses /optimized=true only */ /** Full quality for showcase/display - uses /optimized=true only */
SHOWCASE: 'showcase', SHOWCASE: 'showcase',
/** In-modal display - caps image width at 2400 (covers the ~1200 CSS px
* main viewer at DPR 2); videos stay full quality */
DISPLAY: 'display',
/** Thumbnail size for cards - uses /width=450,optimized=true */ /** Thumbnail size for cards - uses /width=450,optimized=true */
THUMBNAIL: 'thumbnail', THUMBNAIL: 'thumbnail',
/** Small thumbnails for the showcase gallery strip (72px display) - uses /width=160,optimized=true */ /** Small thumbnails for the showcase gallery strip (72px display) - uses /width=160,optimized=true */
@@ -97,15 +100,20 @@ export function rewriteCivitaiUrl(sourceUrl, mediaType = null, mode = Optimizati
} }
// Determine replacement based on mode and media type // Determine replacement based on mode and media type
const isVideo = Boolean(mediaType && mediaType.toLowerCase() === 'video');
let replacement; let replacement;
if (mode === OptimizationMode.SHOWCASE) { if (mode === OptimizationMode.SHOWCASE) {
// Full quality for showcase - no width restriction // Full quality for showcase - no width restriction
replacement = '/optimized=true'; replacement = '/optimized=true';
} else if (mode === OptimizationMode.DISPLAY) {
// Display mode caps image width for in-modal viewing; videos stay
// full quality (CDN transcoding costs more than it saves here)
replacement = isVideo ? '/optimized=true' : '/width=2400,optimized=true';
} else { } else {
// Thumbnail modes with width restriction // Thumbnail modes with width restriction
const width = mode === OptimizationMode.GALLERY_THUMBNAIL ? 160 : 450; const width = mode === OptimizationMode.GALLERY_THUMBNAIL ? 160 : 450;
replacement = `/width=${width},optimized=true`; replacement = `/width=${width},optimized=true`;
if (mediaType && mediaType.toLowerCase() === 'video') { if (isVideo) {
replacement = `/transcode=true,width=${width},optimized=true`; replacement = `/transcode=true,width=${width},optimized=true`;
} }
} }
@@ -153,6 +161,19 @@ export function getShowcaseUrl(url, type = 'image') {
return getOptimizedUrl(url, type, OptimizationMode.SHOWCASE); return getOptimizedUrl(url, type, OptimizationMode.SHOWCASE);
} }
/**
* Get display-optimized URL for the in-modal main viewer (images capped at
* width=2400; videos full quality). Use getShowcaseUrl for full-size viewing
* (e.g. the media viewer overlay)
*
* @param {string} url - Original URL
* @param {string} type - Media type ("image" or "video")
* @returns {string} - Optimized URL for in-modal display
*/
export function getDisplayUrl(url, type = 'image') {
return getOptimizedUrl(url, type, OptimizationMode.DISPLAY);
}
/** /**
* Get thumbnail-optimized URL (width=450) * Get thumbnail-optimized URL (width=450)
* *
+34
View File
@@ -8,6 +8,7 @@ import {
rewriteCivitaiUrl, rewriteCivitaiUrl,
getOptimizedUrl, getOptimizedUrl,
getShowcaseUrl, getShowcaseUrl,
getDisplayUrl,
getThumbnailUrl, getThumbnailUrl,
getGalleryThumbnailUrl, getGalleryThumbnailUrl,
extractCivitaiImageId, extractCivitaiImageId,
@@ -22,6 +23,7 @@ describe('civitaiUtils', () => {
describe('OptimizationMode', () => { describe('OptimizationMode', () => {
it('should have correct mode values', () => { it('should have correct mode values', () => {
expect(OptimizationMode.SHOWCASE).toBe('showcase'); expect(OptimizationMode.SHOWCASE).toBe('showcase');
expect(OptimizationMode.DISPLAY).toBe('display');
expect(OptimizationMode.THUMBNAIL).toBe('thumbnail'); expect(OptimizationMode.THUMBNAIL).toBe('thumbnail');
expect(OptimizationMode.GALLERY_THUMBNAIL).toBe('gallery-thumbnail'); expect(OptimizationMode.GALLERY_THUMBNAIL).toBe('gallery-thumbnail');
}); });
@@ -125,6 +127,22 @@ describe('civitaiUtils', () => {
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=160,optimized=true/12345.mp4'); expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=160,optimized=true/12345.mp4');
}); });
it('should rewrite image URLs with /original=true for display mode (width=2400)', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.DISPLAY);
expect(wasRewritten).toBe(true);
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=2400,optimized=true/12345.jpeg');
});
it('should keep videos full quality in display mode (no transcode/width)', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'video', OptimizationMode.DISPLAY);
expect(wasRewritten).toBe(true);
expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.mp4');
});
it('should not rewrite URLs without /original=true', () => { it('should not rewrite URLs without /original=true', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450/12345.jpeg'; const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450/12345.jpeg';
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL); const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
@@ -250,6 +268,22 @@ describe('civitaiUtils', () => {
}); });
}); });
describe('getDisplayUrl', () => {
it('should return display-optimized URL (width=2400) for images', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
const displayUrl = getDisplayUrl(originalUrl, 'image');
expect(displayUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=2400,optimized=true/12345.jpeg');
});
it('should keep videos full quality', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4';
const displayUrl = getDisplayUrl(originalUrl, 'video');
expect(displayUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.mp4');
});
});
describe('getGalleryThumbnailUrl', () => { describe('getGalleryThumbnailUrl', () => {
it('should return gallery-thumbnail-optimized URL (width=160)', () => { it('should return gallery-thumbnail-optimized URL (width=160)', () => {
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg'; const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';