Overview
┌─────────────────────────────────────────────────────────────┐
│ IMAGE TRANSFORMATION │
├─────────────────────────────────────────────────────────────┤
│ │
│ Original Image (3000x2000, 2MB) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Supabase Image Transformer │ │
│ │ │ │
│ │ /render/image/public/bucket/img │ │
│ │ ?width=400&height=300&quality=80 │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Transformed Image (400x300, 50KB) │
│ │
│ Features: │
│ - Resize (width, height) │
│ - Quality adjustment │
│ - Format conversion (webp) │
│ - CDN caching │
│ │
└─────────────────────────────────────────────────────────────┘
Resize Image
// Get transformed URL
const { data } = supabase.storage
.from('images')
.getPublicUrl('photo.jpg', {
transform: {
width: 400,
height: 300,
},
});
// URL includes transformation params
// .../render/image/public/images/photo.jpg?width=400&height=300
Resize Options
// Width only (maintain aspect ratio)
transform: { width: 400 }
// Height only (maintain aspect ratio)
transform: { height: 300 }
// Both (may crop or fit)
transform: { width: 400, height: 300 }
// Resize modes
transform: {
width: 400,
height: 300,
resize: 'cover', // Default: crop to fill
// resize: 'contain', // Fit within bounds
// resize: 'fill', // Stretch to fill
}
Adjust Quality
// Lower quality = smaller file size
const { data } = supabase.storage
.from('images')
.getPublicUrl('photo.jpg', {
transform: {
width: 800,
quality: 75, // 1-100, default 80
},
});
// Convert to WebP (better compression)
const { data } = supabase.storage
.from('images')
.getPublicUrl('photo.jpg', {
transform: {
width: 800,
format: 'webp',
},
});
// Formats: 'origin' (keep original), 'webp'
// For private buckets, use createSignedUrl
const { data, error } = await supabase.storage
.from('private-images')
.createSignedUrl('photo.jpg', 3600, {
transform: {
width: 400,
height: 400,
quality: 80,
},
});
// data.signedUrl contains transformation
React Image Component
Responsive Image
function OptimizedImage({
bucket,
path,
alt,
sizes,
}: {
bucket: string;
path: string;
alt: string;
sizes: { width: number; quality?: number }[];
}) {
const supabase = createClient();
// Generate srcSet for responsive images
const srcSet = sizes
.map(({ width, quality = 80 }) => {
const { data } = supabase.storage
.from(bucket)
.getPublicUrl(path, {
transform: { width, quality },
});
return `${data.publicUrl} ${width}w`;
})
.join(', ');
// Default image
const { data: defaultData } = supabase.storage
.from(bucket)
.getPublicUrl(path, {
transform: { width: sizes[0].width },
});
return (
<img
src={defaultData.publicUrl}
srcSet={srcSet}
sizes="(max-width: 768px) 100vw, 50vw"
alt={alt}
loading="lazy"
/>
);
}
// Usage
<OptimizedImage
bucket="images"
path="hero.jpg"
alt="Hero image"
sizes={[
{ width: 400, quality: 75 },
{ width: 800, quality: 80 },
{ width: 1200, quality: 85 },
]}
/>
Avatar Component
function Avatar({
userId,
size = 'md',
}: {
userId: string;
size?: 'sm' | 'md' | 'lg';
}) {
const supabase = createClient();
const dimensions = {
sm: 32,
md: 48,
lg: 96,
};
const dimension = dimensions[size];
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.jpg`, {
transform: {
width: dimension,
height: dimension,
resize: 'cover',
},
});
return (
<img
src={data.publicUrl}
alt="Avatar"
width={dimension}
height={dimension}
className="avatar rounded-full"
// Fallback for missing avatar
onError={(e) => {
e.currentTarget.src = '/default-avatar.png';
}}
/>
);
}
Next.js Image Integration
With next/image
import Image from 'next/image';
function ProductImage({ path }: { path: string }) {
const supabase = createClient();
// Get base URL (without transforms - Next.js handles optimization)
const { data } = supabase.storage
.from('products')
.getPublicUrl(path);
return (
<Image
src={data.publicUrl}
alt="Product"
width={400}
height={300}
// Next.js image optimization
quality={80}
placeholder="blur"
blurDataURL="/placeholder.png"
/>
);
}
Custom Loader for Supabase
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/supabase-image-loader.ts',
},
};
// lib/supabase-image-loader.ts
export default function supabaseLoader({
src,
width,
quality,
}: {
src: string;
width: number;
quality?: number;
}) {
// Add transform params to Supabase URL
const url = new URL(src);
url.searchParams.set('width', width.toString());
url.searchParams.set('quality', (quality || 75).toString());
return url.toString();
}
Constraints
┌─────────────────────────────────────────────────────────────┐
│ TRANSFORMATION LIMITS │
├─────────────────────────────────────────────────────────────┤
│ │
│ Max dimensions: │
│ - Width: 2500px │
│ - Height: 2500px │
│ │
│ Supported formats: │
│ - Input: JPEG, PNG, WebP, GIF, AVIF │
│ - Output: JPEG, PNG, WebP │
│ │
│ Caching: │
│ - Transformed images are cached at CDN edge │
│ - First request may be slower │
│ - Subsequent requests are fast │
│ │
│ Note: Transformations available on Pro plan+ │
│ │
└─────────────────────────────────────────────────────────────┘
Best Practices
// ✅ GOOD: Define sizes based on use case
const thumbnailSize = { width: 150, height: 150 };
const previewSize = { width: 400, height: 300 };
const fullSize = { width: 1200 };
// ✅ GOOD: Use WebP for better compression
transform: { format: 'webp', quality: 80 }
// ✅ GOOD: Lazy load images below fold
<img loading="lazy" />
// ❌ BAD: Requesting huge images for small displays
transform: { width: 2000 } // When displaying at 200px
Caching Strategy
// Images are cached by transformation params
// Same params = same cached result
// Use consistent sizes
const AVATAR_SIZES = {
small: { width: 40, height: 40 },
medium: { width: 80, height: 80 },
large: { width: 160, height: 160 },
};
// Avoid random sizes that create cache misses
Tổng kết
| Option |
Values |
Description |
width |
1-2500 |
Width in pixels |
height |
1-2500 |
Height in pixels |
resize |
cover, contain, fill |
Resize mode |
quality |
1-100 |
Image quality |
format |
origin, webp |
Output format |
Usage Pattern
// Public bucket
supabase.storage
.from('bucket')
.getPublicUrl('path', { transform: {...} });
// Private bucket
supabase.storage
.from('bucket')
.createSignedUrl('path', expiry, { transform: {...} });
Q&A
- Cần những kích thước ảnh nào?
- Có dùng WebP được không (browser support)?
- Cần optimize cho mobile không?