Opal Editor automatically optimizes images to reduce storage usage and improve loading performance. The image optimization system runs entirely in the browser using service workers and the Cache API.
Image Cache Architecture
Images are cached using the browser’s Cache API, which provides fast, persistent storage for binary data:
// From ImageCache.ts
export class ImageCache {
name : string ;
guid : string ;
_cache : Promise < Cache > | null = null ;
constructor ({ guid , name } : { guid : string ; name : string }) {
this . guid = guid ;
this . name = name ;
}
private getCacheId = () => ` ${ this . guid } / ${ this . name } ` ;
getCache () {
return ( this . _cache ??= caches . open ( this . getCacheId ()));
}
async destroy () {
await caches . delete ( this . getCacheId ());
}
}
Cache Organization
Each workspace has its own isolated image cache:
workspace-guid-123/img → Main image cache
workspace-guid-123/thumbs → Thumbnail cache
workspace-guid-123/optimized → Optimized versions
Automatic WebP Conversion
When images are served through the service worker, they’re automatically converted to WebP format for better compression:
// Service worker image handling
app . get ( '/api/workspace/:name/image/*' , async ( c ) => {
const imagePath = c . req . param ( '*' );
const workspace = await resolveWorkspace ( c );
// Check cache first
const cache = await ImageCache . getCache ( workspace . id );
const cached = await cache . match ( imagePath );
if ( cached ) {
return cached ;
}
// Load original image
const imageBuffer = await workspace . readFile ( imagePath );
// Convert to WebP
const webpBuffer = await convertToWebP ( imageBuffer , {
quality: 85 ,
lossless: false
});
// Cache the result
const response = new Response ( webpBuffer , {
headers: {
'Content-Type' : 'image/webp' ,
'Cache-Control' : 'max-age=31536000, immutable'
}
});
await cache . put ( imagePath , response . clone ());
return response ;
});
WebP Benefits
Smaller Files WebP provides 25-35% better compression than JPEG/PNG
Faster Loading Smaller files mean faster page loads and less bandwidth
Better Quality Maintains visual quality at lower file sizes
Transparency Supports alpha channel like PNG
Image Processing Pipeline
1. Upload
When images are added to a workspace:
// From handleImageUpload.ts
app . post ( '/api/workspace/:name/image/upload' , async ( c ) => {
const formData = await c . req . formData ();
const file = formData . get ( 'image' ) as File ;
// Validate image
if ( ! file . type . startsWith ( 'image/' )) {
return c . json ({ error: 'Invalid file type' }, 400 );
}
// Read file data
const arrayBuffer = await file . arrayBuffer ();
const uint8Array = new Uint8Array ( arrayBuffer );
// Generate unique filename
const filename = ` ${ Date . now () } - ${ file . name } ` ;
const imagePath = `/images/ ${ filename } ` ;
// Save to workspace
const workspace = await resolveWorkspace ( c );
await workspace . writeFile ( imagePath , uint8Array );
return c . json ({
path: imagePath ,
url: `/api/workspace/ ${ workspace . name } /image ${ imagePath } `
});
});
2. Storage
Images are stored in their original format in the workspace storage (IndexedDB/OPFS).
3. Serving
On request, images are:
Retrieved from storage
Converted to WebP (if not already cached)
Cached for future requests
Served to the browser
4. Cleanup
When images are deleted:
app . delete ( '/api/workspace/:name/image/*' , async ( c ) => {
const imagePath = c . req . param ( '*' );
const workspace = await resolveWorkspace ( c );
// Delete from workspace
await workspace . removeFile ( imagePath );
// Clear from cache
const cache = await ImageCache . getCache ( workspace . id );
await cache . delete ( imagePath );
return c . json ({ success: true });
});
Thumbnail Generation
Opal automatically generates thumbnails for image previews:
// From Thumb.ts
class Thumb {
static async generate (
imageData : Uint8Array ,
options : {
width ?: number ;
height ?: number ;
quality ?: number ;
} = {}
) : Promise < Uint8Array > {
const {
width = 200 ,
height = 200 ,
quality = 80
} = options ;
// Create canvas
const canvas = document . createElement ( 'canvas' );
const ctx = canvas . getContext ( '2d' );
// Load image
const img = new Image ();
const blob = new Blob ([ imageData ]);
const url = URL . createObjectURL ( blob );
await new Promise (( resolve ) => {
img . onload = resolve ;
img . src = url ;
});
// Calculate dimensions maintaining aspect ratio
const scale = Math . min ( width / img . width , height / img . height );
canvas . width = img . width * scale ;
canvas . height = img . height * scale ;
// Draw and compress
ctx . drawImage ( img , 0 , 0 , canvas . width , canvas . height );
// Convert to WebP
const thumbnailBlob = await new Promise < Blob >(( resolve ) => {
canvas . toBlob ( resolve , 'image/webp' , quality / 100 );
});
URL . revokeObjectURL ( url );
return new Uint8Array ( await thumbnailBlob . arrayBuffer ());
}
}
Thumbnail uses:
File browser previews
Grid view in image galleries
Quick loading in markdown preview
Reduced memory usage
Optimization Strategies
Lazy Loading
Images are only loaded when they enter the viewport:
< img
src = "/api/workspace/my-blog/image/photos/large.jpg"
loading = "lazy"
alt = "Large image"
/>
Responsive Images
Serve different sizes based on viewport:
< img
srcset = "
/api/workspace/my-blog/image/photo-400.jpg 400w,
/api/workspace/my-blog/image/photo-800.jpg 800w,
/api/workspace/my-blog/image/photo-1200.jpg 1200w
"
sizes = "(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
src = "/api/workspace/my-blog/image/photo-800.jpg"
alt = "Responsive image"
/>
Progressive Enhancement
Provide fallbacks for browsers without WebP support:
< picture >
< source
srcset = "/api/workspace/my-blog/image/photo.webp"
type = "image/webp"
/>
< source
srcset = "/api/workspace/my-blog/image/photo.jpg"
type = "image/jpeg"
/>
< img
src = "/api/workspace/my-blog/image/photo.jpg"
alt = "Photo with WebP fallback"
/>
</ picture >
Cache Management
Cache Size Limits
Monitor and manage cache size:
async function getCacheSize () : Promise < number > {
if ( 'storage' in navigator && 'estimate' in navigator . storage ) {
const estimate = await navigator . storage . estimate ();
return estimate . usage || 0 ;
}
return 0 ;
}
async function clearImageCache ( workspaceId : string ) {
const cacheId = ImageCache . getCacheId ( workspaceId );
await caches . delete ( cacheId );
}
Cache Invalidation
Invalidate cache when images are updated:
app . put ( '/api/workspace/:name/image/*' , async ( c ) => {
const imagePath = c . req . param ( '*' );
const workspace = await resolveWorkspace ( c );
// Update image in workspace
const formData = await c . req . formData ();
const file = formData . get ( 'image' ) as File ;
const arrayBuffer = await file . arrayBuffer ();
await workspace . writeFile ( imagePath , new Uint8Array ( arrayBuffer ));
// Invalidate cache
const cache = await ImageCache . getCache ( workspace . id );
await cache . delete ( imagePath );
return c . json ({ success: true });
});
Selective Caching
Only cache images above a certain size threshold:
const MIN_CACHE_SIZE = 10 * 1024 ; // 10 KB
if ( imageBuffer . length > MIN_CACHE_SIZE ) {
await cache . put ( imagePath , response . clone ());
}
Compression Ratios
Typical file size reductions:
Original Format Original Size WebP Size Savings JPEG 500 KB 325 KB 35% PNG (photo) 800 KB 520 KB 35% PNG (graphics) 200 KB 140 KB 30%
With caching enabled:
First load: Original image load time
Subsequent loads: < 10ms (cache hit)
WebP conversion: 50-200ms depending on image size
Best Practices
Pre-optimize images before adding to workspace:
Resize to appropriate dimensions
Use image editing tools to compress
Remove EXIF data if not needed
Set appropriate cache headers: 'Cache-Control' : 'max-age=31536000, immutable'
Periodically check and clean up old cached images: const size = await getCacheSize ();
if ( size > CACHE_LIMIT ) {
await pruneOldEntries ();
}
Advanced Configuration
Custom Quality Settings
Adjust WebP quality based on image type:
function getQualityForImage ( imagePath : string ) : number {
if ( imagePath . includes ( '/photos/' )) {
return 85 ; // High quality for photos
} else if ( imagePath . includes ( '/thumbnails/' )) {
return 70 ; // Lower quality for thumbnails
} else {
return 80 ; // Default quality
}
}
Batch Optimization
Optimize multiple images at once:
async function optimizeAllImages ( workspace : Workspace ) {
const images = workspace . getImages ();
for ( const imagePath of images ) {
const imageData = await workspace . readFile ( imagePath );
const optimized = await convertToWebP ( imageData );
// Store optimized version
await workspace . writeFile (
imagePath . replace ( / \. [ ^ . ] + $ / , '.webp' ),
optimized
);
}
}
Smart Caching Strategy
Implement intelligent cache eviction:
class SmartImageCache {
private accessTimes = new Map < string , number >();
async put ( key : string , response : Response ) {
const cache = await this . getCache ();
// Record access time
this . accessTimes . set ( key , Date . now ());
// Evict least recently used if cache is full
if ( await this . isCacheFull ()) {
await this . evictLRU ();
}
await cache . put ( key , response );
}
private async evictLRU () {
// Find least recently used entry
let oldestKey : string | null = null ;
let oldestTime = Date . now ();
for ( const [ key , time ] of this . accessTimes . entries ()) {
if ( time < oldestTime ) {
oldestTime = time ;
oldestKey = key ;
}
}
if ( oldestKey ) {
const cache = await this . getCache ();
await cache . delete ( oldestKey );
this . accessTimes . delete ( oldestKey );
}
}
}
The image optimization system is completely automatic. Users don’t need to manually optimize images — Opal handles it all transparently.