@thddrew/wasm-image-compressor
v0.2.1
Published
Client-side image compression using Web Workers (with a WASM-ready architecture).
Maintainers
Readme
@thddrew/wasm-image-compressor
Client-side image compression using a Web Worker (WASM-ready architecture).
Install
npm install @thddrew/wasm-image-compressorUsage
import { ImageCompressor } from '@thddrew/wasm-image-compressor';
const compressor = new ImageCompressor();
const blob = await compressor.compress(file, {
quality: 75,
mimeType: 'image/jpeg',
});
// Remember to clean up when you’re done (SPA route transitions, etc.)
compressor.dispose();Batch compression
import { ImageCompressor } from '@thddrew/wasm-image-compressor';
const compressor = new ImageCompressor();
const results = await compressor.compressBatch(
files,
{ quality: 75, mimeType: 'image/webp' },
4,
(index, status) => {
console.log(index, status); // processing | completed | error
},
);API reference
The package exports:
ImageCompressor(class)CompressionConfig(type)CompressorOptions(type)
new ImageCompressor(options?: CompressorOptions)
Creates (and initializes) a Web Worker in the browser.
CompressorOptions:
wasmUrl?: string- Optional base URL to load WASM codec binaries from. When provided, the compressor will prefer a module-based worker and attempt to encode using WASM codecs (with an automatic fallback to the OffscreenCanvas encoder if WASM loading/encoding fails).
WASM setup (optional)
If you provide wasmUrl, you must host the codec .wasm files at that base URL. You can copy them from these installed package paths:
@jsquash/jpeg:node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm@jsquash/webp:node_modules/@jsquash/webp/codec/enc/webp_enc.wasm(and optionallywebp_enc_simd.wasm)@jsquash/png:node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm
If you want a ready-made, pinned CDN base URL, this repo publishes those files via jsDelivr:
https://cdn.jsdelivr.net/gh/thddrew/[email protected]/codecs
Example:
import { ImageCompressor } from '@thddrew/wasm-image-compressor';
const compressor = new ImageCompressor({
wasmUrl: '/codecs', // where the .wasm files are hosted
});ImageCompressor#compress(file: File, config?: CompressionConfig): Promise<Blob>
Compresses a single File off the main thread and returns a Blob containing the encoded image bytes.
CompressionConfig:
quality?: number- Range:
0–100 - Default:
75 - Note: in the current worker implementation,
0is treated as “not provided” and will fall back to75.
- Range:
mimeType?: 'image/jpeg' | 'image/png' | 'image/webp'- Default:
'image/jpeg'
- Default:
width?: number- Reserved for future resizing. Currently not applied by the worker.
Notes:
- The returned
Blobusesconfig.mimeTypeif provided; otherwise it uses'image/jpeg'. - This API is browser-only (requires
Worker,Blob,createImageBitmap, andOffscreenCanvas).
ImageCompressor#compressBatch(files: File[], config?: CompressionConfig, maxConcurrency?: number, onProgress?: (index: number, status: 'processing' | 'completed' | 'error') => void): Promise<(Blob | null)[]>
Compresses a list of files concurrently (default maxConcurrency is 4).
- Returns an array aligned with the input
filesarray. - Any file that fails to compress becomes
nullin the returned array. onProgressis called with the file’s index and status:'processing' | 'completed' | 'error'.
ImageCompressor#dispose(): void
Terminates the worker, revokes the internal Blob URL, and clears pending requests. Call this when the compressor is no longer needed.
Performance benchmarks
Benchmark results comparing WASM codec mode vs OffscreenCanvas-only mode (tested on macOS, Chrome 120+):
Test conditions
- Input: 2.0 MB JPEG (2,045,670 bytes)
- Quality: 75%
- Format: JPEG
- Iterations: 20 measured runs (plus 1 warmup)
- Environment: Single image, sequential compression
Results
| Mode | Median time | P95 time | Output size | Compression ratio | |------|-------------|----------|-------------|-------------------| | WASM (MozJPEG) | ~792ms | ~803ms | 710 KB | 65% reduction | | OffscreenCanvas (native) | ~219ms | ~222ms | 992 KB | 52% reduction |
Trade-offs
WASM mode advantages:
- Better compression: ~40% smaller files at the same quality setting
- Consistent results: Same output across browsers
- Advanced codecs: Access to MozJPEG, optimized WebP encoders, etc.
OffscreenCanvas mode advantages:
- Faster: ~3.6x faster compression (219ms vs 792ms median)
- No setup: No need to host
.wasmfiles - Native optimization: Browser's encoder is highly optimized
Why WASM is slower
The WASM path has additional overhead:
- Data copying:
getImageData()copies pixel data from GPU → CPU (~2.4MB for an 800×600 image) - WASM function calls: Crossing the JavaScript/WASM boundary
- Codec implementation: MozJPEG prioritizes compression quality over raw speed
The native browser encoder works directly with GPU textures and is heavily optimized, making it faster despite potentially larger output files.
When to use each mode
- Use WASM mode when: file size matters more than speed (e.g., bandwidth-constrained uploads, storage optimization)
- Use OffscreenCanvas mode when: speed is critical and file size is acceptable (e.g., real-time previews, batch processing with time constraints)
The library automatically falls back to OffscreenCanvas if WASM fails to load or encode, so you can safely default to WASM mode for best compression.
Notes / constraints
- Browser-only: requires
Worker,Blob,createImageBitmap, andOffscreenCanvas. - Bundler-friendly worker: the worker is embedded as a string and started via a
Blob URL, so consumers typically don’t need special worker config. - WASM: when
wasmUrlis provided, the compressor will attempt to load WASM codec binaries and use them for encoding (with a fallback toOffscreenCanvasencoding).
