Back to catalog

responsive-image-optimizer

Generate responsive images with Sharp (WebP, multiple sizes, srcset)

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: responsive-image-optimizer
description: Generate responsive images with Sharp (WebP, multiple sizes, srcset)
activation:
  keywords:
    - responsive images
    - image optimization
    - webp
    - sharp
    - srcset
  triggers:
    - "generate-images"
  auto_load_project: cv-astro
---

# Responsive Image Optimizer

## What This Is

Generate responsive, optimized images using Sharp:
- Multiple sizes (100×100, 300×300, 600×600, 1200×1200)
- WebP conversion for modern browsers
- srcset generation
- Lazy loading attributes
- Automatic resizing and cropping

**For**: CV_Astro project (Sharp image processing)

---

## When to Activate

Auto-activates when keywords detected:
- "optimize images"
- "generate responsive images"
- "create webp versions"
- "image sizes"

Manual: `npm run generate-images`

---

## Image Generation Script

**File**: `scripts/generate-profile-images.js`

```javascript
import sharp from 'sharp';
import fs from 'fs';
import path from 'path';

const INPUT_DIR = 'src/assets/images';
const OUTPUT_DIR = 'public/images';

const SIZES = [
  { name: 'thumbnail', width: 100, height: 100 },
  { name: 'small', width: 300, height: 300 },
  { name: 'medium', width: 600, height: 600 },
  { name: 'large', width: 1200, height: 1200 }
];

const FORMATS = ['webp', 'jpg'];

async function generateResponsiveImages() {
  // Ensure output directory exists
  if (!fs.existsSync(OUTPUT_DIR)) {
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  }

  // Get all source images
  const files = fs.readdirSync(INPUT_DIR)
    .filter(file => /\.(jpg|jpeg|png)$/i.test(file));

  console.log(`📸 Processing ${files.length} images...\n`);

  for (const file of files) {
    const inputPath = path.join(INPUT_DIR, file);
    const baseName = path.parse(file).name;

    console.log(`Processing: ${file}`);

    for (const size of SIZES) {
      for (const format of FORMATS) {
        const outputName = `${baseName}-${size.name}.${format}`;
        const outputPath = path.join(OUTPUT_DIR, outputName);

        try {
          await sharp(inputPath)
            .resize(size.width, size.height, {
              fit: 'cover',
              position: 'center'
            })
            .toFormat(format, {
              quality: format === 'webp' ? 80 : 85
            })
            .toFile(outputPath);

          console.log(`  ✅ ${outputName}`);
        } catch (error) {
          console.error(`  ❌ Error generating ${outputName}:`, error.message);
        }
      }
    }

    console.log('');
  }

  console.log('✨ Image generation complete!\n');
}

generateResponsiveImages().catch(console.error);
```

---

## Responsive Image Component

### React Component

**File**: `src/components/ResponsiveProfileImage.tsx`

```tsx
import React from 'react';
import type { FC } from 'react';

interface ResponsiveProfileImageProps {
  src: string;
  alt: string;
  sizes?: string;
  className?: string;
  priority?: boolean;
}

const ResponsiveProfileImage: FC<ResponsiveProfileImageProps> = ({
  src,
  alt,
  sizes = '100vw',
  className = '',
  priority = false
}) => {
  const baseName = src.replace(/\.(jpg|jpeg|png|webp)$/i, '');

  return (
    <picture>
      <source
        type="image/webp"
        srcSet={`
          ${baseName}-thumbnail.webp 100w,
          ${baseName}-small.webp 300w,
          ${baseName}-medium.webp 600w,
          ${baseName}-large.webp 1200w
        `}
        sizes={sizes}
      />
      <source
        type="image/jpeg"
        srcSet={`
          ${baseName}-thumbnail.jpg 100w,
          ${baseName}-small.jpg 300w,
          ${baseName}-medium.jpg 600w,
          ${baseName}-large.jpg 1200w
        `}
        sizes={sizes}
      />
      <img
        src={`${baseName}-medium.jpg`}
        alt={alt}
        className={className}
        loading={priority ? 'eager' : 'lazy'}
        decoding="async"
      />
    </picture>
  );
};

export default ResponsiveProfileImage;
```

**Usage**:
```astro
---
import ResponsiveProfileImage from '../components/ResponsiveProfileImage.tsx';
---

<ResponsiveProfileImage
  client:load
  src="/images/profile"
  alt="Oriol Macías - Full Stack Developer"
  sizes="(max-width: 768px) 100vw, 50vw"
  priority={true}
/>
```

---

### Astro Component

**File**: `src/components/OptimizedImage.astro`

```astro
---
interface Props {
  src: string;
  alt: string;
  sizes?: string;
  className?: string;
  priority?: boolean;
}

const { src, alt, sizes = '100vw', className = '', priority = false } = Astro.props;
const baseName = src.replace(/\.(jpg|jpeg|png|webp)$/i, '');
---

<picture>
  <source
    type="image/webp"
    srcset={`
      ${baseName}-thumbnail.webp 100w,
      ${baseName}-small.webp 300w,
      ${baseName}-medium.webp 600w,
      ${baseName}-large.webp 1200w
    `}
    sizes={sizes}
  />
  <source
    type="image/jpeg"
    srcset={`
      ${baseName}-thumbnail.jpg 100w,
      ${baseName}-small.jpg 300w,
      ${baseName}-medium.jpg 600w,
      ${baseName}-large.jpg 1200w
    `}
    sizes={sizes}
  />
  <img
    src={`${baseName}-medium.jpg`}
    alt={alt}
    class={className}
    loading={priority ? 'eager' : 'lazy'}
    decoding="async"
  />
</picture>
```

**Usage**:
```astro
---
import OptimizedImage from '../components/OptimizedImage.astro';
---

<OptimizedImage
  src="/images/profile"
  alt="Oriol Macías"
  sizes="(max-width: 768px) 100vw, 50vw"
  priority={true}
/>
```

---

## Sizes Attribute Guide

### Common Patterns

```html
<!-- Full width on mobile, 50% on desktop -->
sizes="(max-width: 768px) 100vw, 50vw"

<!-- Full width on mobile, 33% on tablet, 25% on desktop -->
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 33vw, 25vw"

<!-- Fixed widths -->
sizes="(max-width: 768px) 400px, 800px"

<!-- Sidebar image -->
sizes="(max-width: 1024px) 100vw, 300px"
```

---

## Advanced Optimization

### Background Images

**Script**: `scripts/generate-background-images.js`

```javascript
import sharp from 'sharp';

const BACKGROUND_SIZES = [
  { name: 'mobile', width: 768 },
  { name: 'tablet', width: 1024 },
  { name: 'desktop', width: 1920 },
  { name: 'large', width: 2560 }
];

async function generateBackgrounds() {
  for (const size of BACKGROUND_SIZES) {
    await sharp('src/assets/hero-background.jpg')
      .resize(size.width, null, { fit: 'cover' })
      .webp({ quality: 80 })
      .toFile(`public/images/hero-${size.name}.webp`);

    console.log(`✅ Generated hero-${size.name}.webp`);
  }
}

generateBackgrounds();
```

**Usage**:
```css
.hero {
  background-image: url('/images/hero-mobile.webp');
}

@media (min-width: 768px) {
  .hero {
    background-image: url('/images/hero-tablet.webp');
  }
}

@media (min-width: 1024px) {
  .hero {
    background-image: url('/images/hero-desktop.webp');
  }
}

@media (min-width: 1920px) {
  .hero {
    background-image: url('/images/hero-large.webp');
  }
}
```

---

### Blur Placeholder

```javascript
async function generateWithPlaceholder(inputPath, baseName) {
  // Generate tiny blur placeholder (20px)
  await sharp(inputPath)
    .resize(20, 20, { fit: 'cover' })
    .blur(10)
    .toFormat('webp', { quality: 20 })
    .toFile(`public/images/${baseName}-placeholder.webp`);

  // Generate full sizes...
}
```

**React Component with Blur**:
```tsx
const [loaded, setLoaded] = useState(false);

<picture>
  <img
    src={`${baseName}-placeholder.webp`}
    alt={alt}
    className={`blur-placeholder ${loaded ? 'hidden' : ''}`}
  />
  <img
    src={`${baseName}-medium.webp`}
    alt={alt}
    onLoad={() => setLoaded(true)}
    className={loaded ? 'loaded' : 'loading'}
  />
</picture>
```

---

## Image Quality Guidelines

### JPEG Quality

```javascript
{
  quality: 85,  // Default
  mozjpeg: true  // Better compression
}
```

**Use cases**:
- Photos, portraits: 85-90
- Screenshots: 80-85
- Icons, graphics: 90-95

### WebP Quality

```javascript
{
  quality: 80,  // Default
  effort: 6  // Compression effort (0-6, higher = better but slower)
}
```

**Use cases**:
- Photos: 75-85
- Screenshots: 70-80
- Icons: 85-95

---

## Package.json Scripts

```json
{
  "scripts": {
    "generate-images": "node scripts/generate-profile-images.js",
    "dev": "npm run generate-images && astro dev",
    "build": "npm run generate-images && astro build"
  }
}
```

---

## Performance Impact

### Before Optimization

```
profile.jpg: 2.5MB (3000×3000)
hero.jpg: 5MB (4000×2000)
```

**Total**: 7.5MB

### After Optimization

```
profile-thumbnail.webp: 5KB (100×100)
profile-small.webp: 15KB (300×300)
profile-medium.webp: 40KB (600×600)
profile-large.webp: 120KB (1200×1200)

hero-mobile.webp: 80KB (768px wide)
hero-desktop.webp: 250KB (1920px wide)
```

**Total**: ~510KB (93% reduction!)

---

## Best Practices

### ✅ DO

1. **Always generate multiple sizes**:
   ```javascript
   SIZES = [100, 300, 600, 1200]
   ```

2. **Use WebP with JPEG fallback**:
   ```html
   <source type="image/webp" srcset="..." />
   <source type="image/jpeg" srcset="..." />
   ```

3. **Set appropriate sizes attribute**:
   ```html
   sizes="(max-width: 768px) 100vw, 50vw"
   ```

4. **Lazy load below-fold images**:
   ```html
   loading="lazy"
   ```

5. **Use eager loading for LCP images**:
   ```html
   loading="eager"
   ```

### ❌ DON'T

1. **Don't serve full-size images to mobile**:
   ```html
   <!-- ❌ Bad -->
   <img src="hero-4000px.jpg" />

   <!-- ✅ Good -->
   <img srcset="..." sizes="..." />
   ```

2. **Don't skip WebP format**:
   ```html
   <!-- ❌ Bad -->
   <img src="image.jpg" />

   <!-- ✅ Good -->
   <picture>
     <source type="image/webp" srcset="image.webp" />
     <img src="image.jpg" />
   </picture>
   ```

3. **Don't lazy load above-fold images**:
   ```html
   <!-- ❌ Bad (LCP image) -->
   <img src="hero.jpg" loading="lazy" />

   <!-- ✅ Good -->
   <img src="hero.jpg" loading="eager" />
   ```

---

## Validation

After generation, check:

- [ ] Multiple sizes generated (100, 300, 600, 1200)
- [ ] WebP versions created
- [ ] File sizes reasonable (<200KB for 1200px)
- [ ] Images sharp (not blurry)
- [ ] Aspect ratios correct
- [ ] No console errors in browser

---

## Quick Reference

| Size | Dimensions | Use Case |
|------|------------|----------|
| **Thumbnail** | 100×100 | List items, avatars |
| **Small** | 300×300 | Mobile, cards |
| **Medium** | 600×600 | Tablet, default |
| **Large** | 1200×1200 | Desktop, retina |

**Format**: WebP (primary) + JPEG (fallback)
**Quality**: WebP 80, JPEG 85
**Lazy Load**: Below-fold only