Package Exports
- magic-webp
- magic-webp/worker
Readme
🎨 magic-webp
Fast WebP image processing in the browser using WebAssembly
Process WebP images (static and animated) directly in the browser with native performance. Built on top of Google's libwebp compiled to WebAssembly.
🎮 Live Demo • Features • Installation • Quick Start • API • Development
✨ Features
- 🖼️ WebP Support — Both static and animated WebP images
- ✂️ Crop — Extract regions (preserves animation frames)
- 📐 Resize — Multiple modes: cover, contain, fill, inside, outside
- 🎚️ Quality Control — Adjustable output quality (0-100, lossless)
- 🚀 Fast — Native libwebp with SIMD optimizations (5-10x faster)
- 🌐 Browser-first — No server required, runs entirely client-side
- 🔒 Thread-safe — Automatic operation queuing for concurrent calls
- 📦 Zero dependencies — Pure WebAssembly, no external libraries
📦 Installation
npm install magic-webp
# or
pnpm add magic-webp
# or
yarn add magic-webp🚀 Quick Start
Recommended: Using Web Worker (Non-blocking UI)
Step 1: Copy worker.js to your public folder
# Copy from node_modules
cp node_modules/magic-webp/src-js/worker.ts public/worker.js
# Or download from GitHub
# https://github.com/medzhidov/magic-webp/blob/master/src-js/worker.tsStep 2: Use the simple API
import { MagicWebpWorker } from 'magic-webp';
// Initialize worker
const webp = new MagicWebpWorker('/worker.js');
// Load image
const file = document.querySelector('input[type="file"]').files[0];
await webp.load(file);
// Resize (returns Blob directly!)
const blob = await webp.resize(400, 400, { mode: 'cover', quality: 75 }); // 75 = balanced (recommended)
// Use the result
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
// Clean up when done
webp.terminate();That's it! No manual Worker management, no message passing, just simple async calls.
✨ Benefits: Non-blocking UI, better performance, automatic request queuing
⚠️ Important: Web Worker Requirements
1. Same-Origin Policy
- Worker file must be served from the same domain as your app
- ❌ Won't work:
new MagicWebpWorker('https://cdn.example.com/worker.js') - ✅ Works:
new MagicWebpWorker('/worker.js')(same domain)
2. Module Type
- Worker must be loaded as ES module (
type: 'module') - Already handled by
MagicWebpWorkerconstructor
3. CORS Headers (if serving from different path)
- If worker is on subdomain, ensure proper CORS headers:
Access-Control-Allow-Origin: *
4. File Serving
- Worker file must be accessible via HTTP/HTTPS
- ❌ Won't work with
file://protocol (local files) - ✅ Use local dev server:
npx serveorpython -m http.server
5. Build Tools
- Vite: Worker is automatically bundled
const webp = new MagicWebpWorker( new URL('./worker.ts', import.meta.url).href );
- Webpack: Use
worker-loaderor native Worker support - Create React App: Place worker in
public/folder
Common Issues:
// ❌ WRONG: Cross-origin
const webp = new MagicWebpWorker('https://cdn.com/worker.js');
// Error: Failed to construct 'Worker': Script at '...' cannot be accessed from origin '...'
// ✅ CORRECT: Same origin
const webp = new MagicWebpWorker('/worker.js');
// ✅ CORRECT: Relative path
const webp = new MagicWebpWorker('./worker.js');
// ✅ CORRECT: Vite/Webpack (bundled)
const webp = new MagicWebpWorker(
new URL('./worker.ts', import.meta.url).href
);Alternative: Main Thread (Simpler, but blocks UI)
import { MagicWebp } from 'magic-webp';
const file = document.querySelector('input[type="file"]').files[0];
const img = await MagicWebp.fromBlob(file);
const resized = await img.resize(400, 400, { mode: 'cover', quality: 75 }); // 75 = balanced
const blob = resized.toBlob();⚠️ Note: Main thread usage blocks the UI during processing. Use
MagicWebpWorkerfor production apps.
📖 API
MagicWebpWorker (Recommended)
import { MagicWebpWorker } from 'magic-webp';
// Initialize
const webp = new MagicWebpWorker('/worker.js');
// Load image
await webp.load(file); // File or Blob
// Get dimensions
console.log(webp.width, webp.height);
// Crop
const blob = await webp.crop(x, y, width, height, quality);
// Resize
const blob = await webp.resize(width, height, { mode, position, quality });
// Clean up
webp.terminate();MagicWebp (Main Thread)
import { MagicWebp } from 'magic-webp';
// Load from File/Blob
const img = await MagicWebp.fromBlob(blob);
const img = await MagicWebp.fromFile(file);
// Load from URL
const img = await MagicWebp.fromUrl('https://example.com/image.webp');
// Load from Uint8Array
const img = await MagicWebp.fromBytes(uint8Array);Transformations
All transformation methods are async and return Promise<MagicWebp>.
Crop
// Crop 200x200 region starting at (50, 50)
const cropped = await img.crop(50, 50, 200, 200, quality);Resize
// Cover - fills dimensions, crops excess (default)
const cover = await img.resize(400, 400, { mode: 'cover' });
// Contain - fits inside dimensions, preserves aspect ratio
const contain = await img.resize(400, 400, { mode: 'contain' });
// Fill - stretches to exact dimensions (may distort)
const fill = await img.resize(400, 400, { mode: 'fill' });
// Inside - like contain, but never enlarges
const inside = await img.resize(400, 400, { mode: 'inside' });
// Outside - like cover, but never reduces
const outside = await img.resize(400, 400, { mode: 'outside' });
// With position (for cover/outside modes)
const banner = await img.resize(1200, 400, {
mode: 'cover',
position: 'top', // 'center', 'top', 'bottom', 'left', 'right', etc.
quality: 75 // 0-100, default 75 (balanced - recommended)
});Output
// As Blob
const blob = img.toBlob();
// As Uint8Array
const bytes = img.toBytes();
// As Data URL
const dataUrl = await img.toDataUrl();
// As Object URL
const objectUrl = img.toObjectUrl();Resize Options
interface ResizeOptions {
mode?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; // default: 'cover'
position?: 'center' | 'top' | 'bottom' | 'left' | 'right' | // default: 'center'
'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
quality?: number; // 0-100, default: 75 (balanced)
}💡 Quality Recommendations:
| Quality | File Size | Visual Quality | Use Case | Recommended For |
|---|---|---|---|---|
| 60-70 | Smallest | Visible artifacts | Thumbnails, previews | Low priority images |
| 75-85 | Medium | Good balance | Most web images | ✅ Default (75) |
| 90-95 | Large | Excellent | Important photos | Hero images, portfolios |
| 100 | Largest | Perfect (lossless) | Archival, editing | When quality is critical |
Quality Recommendations
- 60-70: High compression, visible artifacts (good for thumbnails)
- 75-85: Balanced quality/size (recommended for most cases)
- 90-95: High quality, minimal artifacts (for important images)
- 100: Lossless, largest file size (perfect quality preservation)
Supported Formats
Input:
- ✅ Static WebP
- ✅ Animated WebP (multi-frame)
Output:
- ✅ Static WebP (from static input)
- ✅ Animated WebP (from animated input, preserves all frames and timing)
Note: All operations (crop, resize) work on both static and animated WebP. For animated images, each frame is processed individually while preserving animation metadata (timing, loop count, etc.).
💡 Examples
With Worker (Recommended)
import { MagicWebpWorker } from 'magic-webp';
const webp = new MagicWebpWorker('/worker.js');
await webp.load(file);
// Avatar - square 200x200, centered (high quality for profile pics)
const avatar = await webp.resize(200, 200, { mode: 'cover', quality: 85 });
// Product preview - fit inside 300x300 (balanced quality)
const preview = await webp.resize(300, 300, { mode: 'contain', quality: 75 });
// Banner - 1200x400, crop from top (high quality for hero images)
const banner = await webp.resize(1200, 400, {
mode: 'cover',
position: 'top',
quality: 90
});
// Thumbnail - never enlarge (lower quality for small images)
const thumb = await webp.resize(150, 150, { mode: 'inside', quality: 65 });
// Crop specific region (balanced quality)
const cropped = await webp.crop(50, 50, 200, 200, 75);
webp.terminate();With Main Thread
import { MagicWebp } from 'magic-webp';
const img = await MagicWebp.fromBlob(file);
// Chaining operations
const result = await img
.crop(100, 100, 400, 400)
.then(cropped => cropped.resize(200, 200, { mode: 'contain' }));
// Concurrent processing (automatically queued)
const [avatar, thumb, banner] = await Promise.all([
img.resize(200, 200, { mode: 'cover' }),
img.resize(150, 150, { mode: 'inside' }),
img.resize(1200, 400, { mode: 'cover', position: 'top' })
]);Animated WebP
// Works with both static and animated WebP
// For animated images, all frames are processed while preserving timing
const webp = new MagicWebpWorker('/worker.js');
await webp.load(animatedWebpFile);
// All frames will be resized
const resized = await webp.resize(400, 400, { mode: 'cover' });🔧 Advanced Usage
Memory Management
// Worker automatically manages memory, but you should terminate when done
const webp = new MagicWebpWorker('/worker.js');
// ... use worker ...
// Clean up when component unmounts or app closes
webp.terminate();Debug Mode
By default, magic-webp runs silently in production (no console logs). Enable debug mode for development:
import { setDebugMode } from 'magic-webp';
// Enable debug logging (disabled by default)
setDebugMode(true);
// Now you'll see detailed logs:
// [magic-webp] Loading Emscripten WASM module...
// [magic-webp] WASM module ready
// [magic-webp] Processing 45678 bytes
// [magic-webp] Cropping: 0,0 200x200, quality: 75
// etc.
// Disable debug mode
setDebugMode(false);💡 Tip: Enable debug mode only during development. In production, logs are automatically disabled for better performance and cleaner console.
Error Handling
const webp = new MagicWebpWorker('/worker.js');
try {
await webp.load(file);
const blob = await webp.resize(400, 400, { mode: 'cover' });
} catch (error) {
console.error('Processing failed:', error);
// Handle error (show message to user, retry, etc.)
}Multiple Workers (Parallel Processing)
// Create multiple workers for parallel processing
const workers = [
new MagicWebpWorker('/worker.js'),
new MagicWebpWorker('/worker.js'),
new MagicWebpWorker('/worker.js')
];
// Process multiple images in parallel
const results = await Promise.all(
files.map((file, i) => {
const worker = workers[i % workers.length];
return worker.load(file).then(() =>
worker.resize(400, 400, { mode: 'cover' })
);
})
);
// Clean up
workers.forEach(w => w.terminate());Browser Compatibility
- ✅ Chrome 80+
- ✅ Firefox 79+
- ✅ Safari 14+
- ✅ Edge 80+
- ❌ IE 11 (no WebAssembly support)
Check before using:
if (typeof WebAssembly === 'undefined') {
console.error('WebAssembly not supported');
// Fallback to server-side processing
}
if (typeof Worker === 'undefined') {
console.warn('Web Workers not supported, using main thread');
// Use MagicWebp instead of MagicWebpWorker
}🛠️ Development
Prerequisites
- Node.js 18+
- pnpm (recommended) or npm
- Emscripten SDK (automatically installed)
Setup
# Clone repository
git clone https://github.com/medzhidov/magic-webp.git
cd magic-webp
# Install dependencies
pnpm install
# Build WASM module
pnpm build:wasm
# Run demo
pnpm demo:watchProject Structure
magic-webp/
├── src-c/ # C source code
│ ├── animation.c # WebP animation processing
│ └── magic_webp.c # Core functions
├── src-js/ # TypeScript API
│ ├── index.ts # Main API
│ └── *.test.ts # Tests
├── demo/ # Demo application
│ ├── index.html
│ ├── main.ts
│ └── worker.ts # Web Worker for processing
├── tests/ # Native C tests
├── libwebp/ # Google's libwebp (submodule)
└── pkg/ # Built WASM outputBuild Commands
# Build WASM module
pnpm build:wasm
# Run TypeScript tests
pnpm test
# Run native C tests
pnpm test:native
# Run demo (dev server)
pnpm demo:watch
# Build demo for production
pnpm demo:buildHow It Works
- C Code (
src-c/) - Uses libwebp's WebPPicture API for high-quality image processing - Animation Support - Processes each frame individually, preserving timing and metadata
- Emscripten - Compiles C code to WebAssembly with SIMD optimizations
- TypeScript API (
src-js/) - Provides clean, type-safe interface - Operation Queue - Ensures thread-safety by serializing WASM calls
- Web Worker (demo) - Keeps UI responsive during processing
Performance
- 5-10x faster than pure JavaScript implementations
- SIMD optimizations (SSE2, NEON) for resize operations
- Optimized cover mode - single pass resize+crop (2x faster)
- Minimal memory - in-place operations where possible
📋 Quick Reference
Resize Modes
| Mode | Behavior | Use Case |
|---|---|---|
cover |
Fills dimensions, crops excess | Avatars, thumbnails |
contain |
Fits inside, preserves aspect | Product images, previews |
fill |
Stretches to exact size | Backgrounds (may distort) |
inside |
Like contain, never enlarges | Thumbnails of small images |
outside |
Like cover, never reduces | Cropping large images |
Position Options (for cover/outside)
| Position | Description |
|---|---|
center |
Center of image (default) |
top |
Top center |
bottom |
Bottom center |
left |
Left center |
right |
Right center |
top-left |
Top left corner |
top-right |
Top right corner |
bottom-left |
Bottom left corner |
bottom-right |
Bottom right corner |
Quality Guidelines
| Quality | File Size | Visual Quality | Use Case |
|---|---|---|---|
| 60-70 | Smallest | Visible artifacts | Thumbnails, previews |
| 75-85 | Medium | Good balance | Most web images |
| 90-95 | Large | Excellent | Important images, photos |
| 100 | Largest | Perfect (lossless) | Archival, editing |
📄 License
MIT © Ilia Medzhidov
🙏 Acknowledgments
- libwebp - Google's WebP library
- Emscripten - C/C++ to WebAssembly compiler
Made with ❤️ for the web