Bỏ qua

Image Transformations

Overview

Transform Flow

┌─────────────────────────────────────────────────────────────┐
│                  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                                              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Basic Transformations

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
}

Quality & Format

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 Format

// Convert to WebP (better compression)
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      format: 'webp',
    },
  });

// Formats: 'origin' (keep original), 'webp'

Signed URL Transformations

Private Bucket Transforms

// 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();
}

Transformation Limits

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

Performance Tips

// ✅ 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

Transform Options

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

  1. Cần những kích thước ảnh nào?
  2. Có dùng WebP được không (browser support)?
  3. Cần optimize cho mobile không?