Image Transformations and CDN
The Need for Image Optimization
Users upload images in various sizes and formats. Serving the original file is often wasteful:
- A 5MB photo when a 50KB thumbnail would do
- A 4000px image displayed at 400px
- JPEG when WebP offers better compression
- Same image requested from servers worldwide
Supabase Storage solves these challenges with built-in transformations and CDN delivery.
Image Transformation Basics
Transform images on-the-fly by adding parameters to the URL:
Base URL:
https://project.supabase.co/storage/v1/object/public/avatars/photo.jpg
Transformed URL:
https://project.supabase.co/storage/v1/render/image/public/avatars/photo.jpg?width=200&height=200
Notice the path change: /object/ → /render/image/
Transformation Options
Resizing
// Width only (maintains aspect ratio)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
width: 200
}
})
// Height only
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
height: 200
}
})
// Both (may crop depending on resize mode)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
width: 200,
height: 200
}
})
Resize Modes
| Mode | Behavior |
|---|---|
cover | Fill dimensions, crop if needed (default) |
contain | Fit within dimensions, may letterbox |
fill | Stretch to fill, may distort |
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
width: 200,
height: 200,
resize: 'contain' // Fit within box
}
})
Format Conversion
// Convert to WebP
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
format: 'webp'
}
})
// Available formats: 'origin', 'webp', 'avif'
Quality
// Reduce quality for smaller files
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
quality: 75 // 1-100
}
})
Combined Transformations
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('photo.jpg', {
transform: {
width: 400,
height: 400,
resize: 'cover',
format: 'webp',
quality: 80
}
})
Common Image Transformation Patterns
Avatar Sizes
function getAvatarUrl(path, size) {
const sizes = {
small: { width: 40, height: 40 },
medium: { width: 100, height: 100 },
large: { width: 200, height: 200 }
}
return supabase.storage
.from('avatars')
.getPublicUrl(path, {
transform: {
...sizes[size],
resize: 'cover',
format: 'webp'
}
}).data.publicUrl
}
// Usage
const smallAvatar = getAvatarUrl('user-123/profile.jpg', 'small')
const largeAvatar = getAvatarUrl('user-123/profile.jpg', 'large')
Responsive Images
function getResponsiveImageUrls(path) {
const widths = [320, 640, 960, 1280, 1920]
return widths.map(width => ({
width,
url: supabase.storage
.from('images')
.getPublicUrl(path, {
transform: { width, format: 'webp' }
}).data.publicUrl
}))
}
// Usage in HTML
const urls = getResponsiveImageUrls('hero.jpg')
// <img srcset="...320w, ...640w, ..." />
Thumbnail Generation
function getThumbnailUrl(path) {
return supabase.storage
.from('uploads')
.getPublicUrl(path, {
transform: {
width: 150,
height: 150,
resize: 'cover',
format: 'webp',
quality: 60
}
}).data.publicUrl
}
CDN and Caching
How the CDN Works
User Request
│
▼
┌─────────────────┐
│ CDN Edge │ ← Geographically close to user
│ (Cache) │
└────────┬────────┘
│
Cache Hit? ────── Yes ──→ Return cached image
│
No
│
▼
┌─────────────────┐
│ Transformation │ ← Process image
│ Server │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Origin │ ← Original file storage
│ (S3) │
└─────────────────┘
Cache Behavior
Transformed images are cached at multiple levels:
- CDN Edge: Closest to users, fastest response
- Transformation Cache: Avoid re-processing same transforms
- Origin: Source file storage
Cache Control Headers
// Set cache duration when uploading
await supabase.storage
.from('assets')
.upload('logo.png', file, {
cacheControl: '31536000' // 1 year in seconds
})
Cache Headers Explained
| Header | Value | Meaning |
|---|---|---|
public | - | CDN can cache |
max-age | seconds | Browser cache duration |
s-maxage | seconds | CDN cache duration |
no-cache | - | Must revalidate |
no-store | - | Never cache |
Performance Best Practices
1. Choose Appropriate Sizes
// Don't do this - full resolution for thumbnail
<img src={getPublicUrl('photo.jpg')} style="width: 50px" />
// Do this - request the size you need
<img src={getPublicUrl('photo.jpg', { transform: { width: 50 } })} />
2. Use Modern Formats
// WebP typically 25-35% smaller than JPEG
const url = supabase.storage
.from('images')
.getPublicUrl('photo.jpg', {
transform: { format: 'webp' }
}).data.publicUrl
// With fallback for older browsers
<picture>
<source srcset={webpUrl} type="image/webp" />
<img src={jpegUrl} />
</picture>
3. Implement Responsive Images
<!-- Let browser choose appropriate size -->
<img
srcset="photo-320.webp 320w,
photo-640.webp 640w,
photo-960.webp 960w"
sizes="(max-width: 600px) 100vw, 50vw"
src="photo-640.webp"
/>
4. Lazy Load Below-Fold Images
<!-- Native lazy loading -->
<img src="..." loading="lazy" alt="Lazy loaded image" />
5. Set Long Cache Times for Immutable Assets
// Versioned filename allows long cache
const filename = `logo-${version}.png`
await supabase.storage
.from('assets')
.upload(filename, file, {
cacheControl: '31536000' // 1 year
})
URL Structure Reference
Public Object URL
https://{project}.supabase.co/storage/v1/object/public/{bucket}/{path}
Authenticated Object URL
https://{project}.supabase.co/storage/v1/object/authenticated/{bucket}/{path}
Transformed Image URL
https://{project}.supabase.co/storage/v1/render/image/public/{bucket}/{path}?width=X&height=Y&format=webp
Signed URL
https://{project}.supabase.co/storage/v1/object/sign/{bucket}/{path}?token={jwt}
Limitations and Considerations
Transformation Limits
- Only works on images (JPEG, PNG, WebP, GIF, AVIF)
- Maximum dimensions vary by plan
- Some transformations have processing limits
Format Support
| Input | Output Formats |
|---|---|
| JPEG | JPEG, WebP, AVIF |
| PNG | PNG, WebP, AVIF |
| WebP | JPEG, PNG, WebP, AVIF |
| GIF | GIF (animated), WebP |
Bandwidth Considerations
- Transformations count toward bandwidth
- Consider pre-generating common sizes
- Cache effectively to reduce repeated transformations
Key Takeaways
- Transform on-demand: No need to store multiple sizes
- Modern formats reduce size: WebP typically 30% smaller
- CDN accelerates delivery: Edge caching worldwide
- Cache aggressively: Long TTLs for static assets
- Request appropriate sizes: Don't download more than needed
- Use responsive images: Let browsers choose the right size
Module Summary
In this module, you've learned:
- Supabase Storage architecture and metadata
- Buckets, objects, and RLS-based access control
- Image transformations for optimization
- CDN caching for performance
Storage is more than a file server—it's a complete media delivery system. With RLS for security, transformations for optimization, and CDN for speed, you can build media-rich applications that perform well globally.
The best image is the one the user doesn't notice loading. With proper sizing, modern formats, and edge caching, you can make heavy images feel instant.

