| title | Image Optimization | |||||
|---|---|---|---|---|---|---|
| description | Automatic image optimization with modern formats, responsive images, and lazy loading | |||||
| section | Plugins | |||||
| difficulty | intermediate | |||||
| tags |
|
Automatic image optimization with modern formats, responsive images, lazy loading, and click-to-zoom.
- Multiple Formats - Auto-generate WebP and AVIF with fallbacks
- Responsive Images - Generate multiple sizes with srcset
- Lazy Loading - IntersectionObserver-based lazy loading
- Blur Placeholder - LQIP (Low Quality Image Placeholder) for smooth loading
- Click-to-Zoom - Full-size lightbox modal
- Automatic Optimization - Works at build time, zero configuration
pnpm add @goobits/docs-engine// svelte.config.js
import { imageOptimizationPlugin } from '@goobits/docs-engine/plugins';
export default {
preprocess: [
mdsvex({
remarkPlugins: [imageOptimizationPlugin()]
})
]
};Use in markdown:
imageOptimizationPlugin({
formats: ['webp', 'avif', 'original'],
sizes: [640, 960, 1280, 1920],
lazy: true,
zoom: true,
placeholder: 'blur'
})Add hydrator to your layout:
<script>
import { OptimizedImageHydrator } from '@goobits/docs-engine/components';
</script>
<slot />
<OptimizedImageHydrator />The plugin accepts {@ImageOptimizationOptions} for comprehensive control over image processing:
interface ImageOptimizationOptions {
/** Base path for optimized images (default: '/images') */
basePath?: string;
/** Image formats to generate (default: ['webp', 'avif', 'original']) */
formats?: Array<'webp' | 'avif' | 'jpg' | 'png' | 'original'>;
/** Responsive image sizes to generate (default: [640, 960, 1280, 1920]) */
sizes?: number[];
/** Quality settings per format (default: { webp: 85, avif: 80, jpg: 85, png: 100 }) */
quality?: Partial<Record<'webp' | 'avif' | 'jpg' | 'png', number>>;
/** Enable lazy loading (default: true) */
lazy?: boolean;
/** Enable click-to-zoom (default: true) */
zoom?: boolean;
/** Placeholder type: 'blur', 'dominant-color', 'none' (default: 'blur') */
placeholder?: 'blur' | 'dominant-color' | 'none';
/** Skip optimization for external URLs (default: true) */
skipExternal?: boolean;
}const defaults = {
basePath: '/images',
formats: ['webp', 'avif', 'original'],
sizes: [640, 960, 1280, 1920],
quality: {
webp: 85,
avif: 80,
jpg: 85,
png: 100
},
lazy: true,
zoom: true,
placeholder: 'blur',
skipExternal: true
};Use when you need programmatic control over image processing:
// scripts/optimize-images.ts
import { processImage } from '@goobits/docs-engine/server';
const result = await processImage({
inputPath: './docs/images/screenshot.png',
outputDir: './static/images/optimized',
formats: ['webp', 'avif', 'original'],
sizes: [640, 960, 1280, 1920],
quality: { webp: 85, avif: 80 },
generatePlaceholder: true
});
console.log('Generated variants:', result.variants);
console.log('Placeholder:', result.placeholder);import { batchProcessImages } from '@goobits/docs-engine/server';
import { glob } from 'glob';
const images = await glob('docs/**/*.{png,jpg,jpeg}');
const results = await batchProcessImages(
images.map((img) => ({
inputPath: img,
outputDir: './static/images/optimized',
formats: ['webp', 'avif', 'original'],
sizes: [640, 960, 1280, 1920],
quality: { webp: 85, avif: 80 },
generatePlaceholder: true
}))
);
console.log(`Optimized ${results.length} images`);Dimensions are extracted from the URL and the clean URL is used for optimization.
External images are passed through unless skipExternal: false.
The plugin transforms markdown images into optimized <picture> elements:
<figure class="optimized-image-container">
<picture>
<!-- AVIF (best compression) -->
<source
type="image/avif"
srcset="/images/photo-640w.avif 640w,
/images/photo-960w.avif 960w,
/images/photo-1280w.avif 1280w"
sizes="(max-width: 640px) 100vw, (max-width: 960px) 90vw, 1280px"
/>
<!-- WebP (wide support) -->
<source
type="image/webp"
srcset="/images/photo-640w.webp 640w,
/images/photo-960w.webp 960w,
/images/photo-1280w.webp 1280w"
sizes="(max-width: 640px) 100vw, (max-width: 960px) 90vw, 1280px"
/>
<!-- Original format (fallback) -->
<img
src="/images/photo-640w.jpg"
alt="Photo description"
loading="lazy"
class="optimized-image zoomable"
/>
</picture>
<figcaption>Photo caption</figcaption>
</figure>Modern formats (WebP, AVIF) provide 30-50% better compression than JPEG/PNG:
- AVIF: Best compression, newer browsers
- WebP: Great compression, wide support
- Original: Fallback for older browsers
Browsers automatically choose the best format they support.
Different image sizes for different screen sizes:
srcset="/img-640w.webp 640w,
/img-960w.webp 960w,
/img-1280w.webp 1280w"- Mobile (375-640px): Loads 640px version
- Tablet (768-960px): Loads 960px version
- Desktop (1024+): Loads 1280px version
Saves bandwidth on mobile devices.
Images below the fold are loaded only when scrolled into view:
- Uses IntersectionObserver (native browser API)
- Starts loading 50px before image enters viewport
- Dramatically improves initial page load time
A tiny, blurred version (40px wide) loads first:
- 1-2KB size (loads instantly)
- Smooth blur-to-sharp transition
- Better perceived performance
Click any image to view full-size in a lightbox:
- Keyboard navigation (Escape to close)
- Backdrop click to close
- Shows image caption
- Smooth animations
graph LR
subgraph Before["❌ Before Optimization"]
B1["Original PNG"]
B2["2.4 MB"]
B3["No responsive images"]
B4["3.2s load on 3G"]
B5["Lighthouse: 65"]
B1 --> B2 --> B3 --> B4 --> B5
end
subgraph After["✅ After Optimization"]
A1["AVIF + WebP + Original"]
A2["180 KB (93% smaller)"]
A3["4 responsive sizes"]
A4["0.4s load on 3G"]
A5["Lighthouse: 95"]
A1 --> A2 --> A3 --> A4 --> A5
end
Before -.->|"imageOptimizationPlugin()"| After
style Before fill:#45283c,stroke:#f38ba8
style After fill:#2d3748,stroke:#a6e3a1
| Metric | Before | After | Improvement |
|---|---|---|---|
| File Size | 2.4 MB | 180 KB | 93% smaller |
| Load Time (3G) | 3.2s | 0.4s | 8x faster |
| Formats | 1 (PNG) | 3 (AVIF, WebP, PNG) | Better compatibility |
| Responsive Sizes | 1 | 4 (640px, 960px, 1280px, 1920px) | Mobile-optimized |
| Lighthouse Score | 65 | 95 | +30 points |
- JPEG → WebP: 30-40% smaller
- PNG → WebP: 50-60% smaller
- WebP → AVIF: 20-30% smaller
- Lazy loading: 40-60% faster initial load
Input (Markdown):
Output (Generated HTML):
<figure class="optimized-image-container">
<picture>
<!-- Modern format: Best compression -->
<source
type="image/avif"
srcset="
/images/dashboard-640w.avif 640w,
/images/dashboard-960w.avif 960w,
/images/dashboard-1280w.avif 1280w,
/images/dashboard-1920w.avif 1920w
"
sizes="(max-width: 640px) 100vw, (max-width: 960px) 90vw, 1280px"
/>
<!-- Widely supported format -->
<source
type="image/webp"
srcset="
/images/dashboard-640w.webp 640w,
/images/dashboard-960w.webp 960w,
/images/dashboard-1280w.webp 1280w,
/images/dashboard-1920w.webp 1920w
"
sizes="(max-width: 640px) 100vw, (max-width: 960px) 90vw, 1280px"
/>
<!-- Fallback for older browsers -->
<img
src="/images/dashboard-960w.png"
alt="Dashboard screenshot showing user analytics"
loading="lazy"
class="optimized-image zoomable"
style="background-image: url(data:image/svg+xml;base64,...)"
/>
</picture>
<figcaption>Dashboard screenshot showing user analytics</figcaption>
</figure>Result:
- 📦 12 files generated (4 sizes × 3 formats)
- 📊 Automatic format selection (browser picks best supported format)
- 📱 Responsive loading (mobile gets 640px, desktop gets 1280px)
- ⚡ Lazy loading (loads only when scrolled into view)
- 🔍 Click-to-zoom (full-size lightbox on click)
- 🎨 Blur placeholder (smooth loading transition)
Image optimization directly improves Core Web Vitals:
- LCP (Largest Contentful Paint) - Faster image loading
- CLS (Cumulative Layout Shift) - Dimensions prevent layout shift
- FCP (First Contentful Paint) - Smaller images load faster
Expected Lighthouse improvements:
- Performance: +15-30 points
- Best Practices: +5-10 points
| Format | Support | Fallback |
|---|---|---|
| AVIF | Chrome 85+, Firefox 93+, Safari 16+ | WebP → Original |
| WebP | 95% of browsers | Original format |
| Original | 100% | Always works |
The <picture> element ensures all users get optimized images.
Check:
- Plugin is added to
remarkPluginsarray - Hydrator component is in your layout
- Image paths are correct
- Build process ran successfully
Set generatePlaceholder: true in build-time processing:
await processImage({
inputPath: './image.png',
outputDir: './static/images',
generatePlaceholder: true
});External images are skipped by default. Enable with:
imageOptimizationPlugin({
skipExternal: false
})Ensure hydrator is present and JavaScript is enabled:
<OptimizedImageHydrator />- Photos: JPEG → WebP → AVIF
- Graphics/UI: PNG → WebP
- Logos: SVG (no optimization needed)
- Icons: SVG or inline SVG
Match your content width:
// For 800px content width
sizes: [400, 800, 1600] // 1x, 2x, 3x for retinaBalance size vs. quality:
- Hero images: 90-95 quality
- Content images: 80-85 quality
- Thumbnails: 70-80 quality
Always provide descriptive alt text:
<!-- Good -->

<!-- Bad -->
docs/
images/
screenshots/
dashboard.png
profile.png
diagrams/
architecture.svg
static/
images/
optimized/ ← Generated files go here
Prerequisites: Basic markdown knowledge, understanding of web image formats
Next Steps:
- Screenshots Plugin - Automated screenshot generation
- Architecture Guide - System design and philosophy
Related:
- Sharp Documentation - Image processing library