@gkzlabs/image-compression

🎮 Try the live demo — 5 framework examples (React, Vue, Svelte, Angular, Vanilla) running in your browser. No install.
Framework-agnostic image compression for the browser.
Pure web APIs. Zero runtime dependencies.
A modern, progressive-enhancement image compression library that runs entirely in the browser using native Web APIs. Works with any frontend framework (Angular, React, Vue, Svelte) or vanilla JS.
✨ Features
- 🚀 4-path cascade — WebCodecs → OffscreenCanvas → Canvas2D → server-fallback
- 🎯 Target-size mode —
maxSizeMB guarantees the output fits under a size budget (auto quality → dimension ladder)
- 🖼️ AVIF / WebP / JPEG / PNG output —
format: 'image/avif' encodes 30-50% smaller than JPEG, with automatic fallback on browsers that can't encode AVIF
- 🔄 Manual rotation —
rotate: 0 | 90 | 180 | 270 (overrides EXIF auto-rotation)
- 🪞 Mirror/flip —
mirror: 'horizontal' | 'vertical'
- 📐 Exact resize —
width / height / keepAspectRatio for precise dimensions
- 🖼️ Auto EXIF rotation — vertical phone photos auto-orient correctly
- 🌊 Streaming API —
compress$() and compressAll$() return native AsyncIterable (no RxJS needed)
- 📦 Framework-agnostic — Zero dependencies on Angular, React, or RxJS
- 🖼️ HEIC decode — Lazy-loaded via
heic2any (optional, ~256 KB)
- ⚡ Smart pass-through — Skip compression for already-small JPEGs (
passThroughUnderBytes)
- 🛑 Cancellable —
AbortSignal support for clean cancellation
- 🧪 Well-tested — 188 unit tests covering all paths and edge cases
- 📱 Mobile-friendly — Bounded concurrency (default 2) prevents OOM on phones
📦 Installation
npm install @gkzlabs/image-compression
npm install git+ssh://git@github.com/gkzlabs/image-compression.git
🚀 Quick Start
Promise-based (vanilla JS)
import { ImageCompression } from '@gkzlabs/image-compression';
const svc = new ImageCompression();
const result = await svc.compress(file, { quality: 0.85, maxWidthOrHeight: 2048 });
console.log(result.file.name);
console.log(result.path);
console.log(result.compressedSize);
svc.dispose();
Streaming (AsyncIterable)
import { compress$ } from '@gkzlabs/image-compression';
for await (const evt of compress$(file, { quality: 0.85 }, svc)) {
if ('percent' in evt) {
console.log(`[${evt.percent}%] ${evt.stage}`);
} else {
console.log('Done:', evt.file.name);
}
}
Angular (wrapper package)
import { ImageCompressionService } from 'angular-image-compression';
@Component({ ... })
export class MyComponent {
private svc = inject(ImageCompressionService);
async onFile(file: File) {
const result = await this.svc.compress(file, { quality: 0.85 });
}
}
📊 API Surface
ImageCompression class
new ImageCompression();
.compress(file: File | Blob, options?: CompressionOptions): Promise<CompressionResult>
.compressAll(files: (File|Blob)[], options?, maxConcurrent?: number): Promise<CompressionResult[]>
.getCapabilities(): Promise<DeviceCapabilities>
.terminate(): void
.dispose(): void
compress$() / compressAll$() streams
compress$(file, options, svc): AsyncIterable<CompressionProgress | CompressionResult>
compressAll$(files, options, maxConcurrent, svc): AsyncIterable<BatchProgress | CompressionResult[]>
Utilities
import {
detectCapabilities,
readExifOrientation,
extensionForMimeType,
applyExifOrientation,
applyRotation,
resizeExact,
} from '@gkzlabs/image-compression';
Transform Helpers (low-level)
For advanced use cases (e.g., custom compression pipelines), the rotate/resize helpers are exported:
import { applyRotation, resizeExact } from '@gkzlabs/image-compression';
const { bitmap, width, height } = applyRotation(bitmap, 90, 'horizontal');
const { bitmap, width, height } = resizeExact(bitmap, 800);
const { bitmap, width, height } = resizeExact(bitmap, undefined, 600);
const { bitmap, width, height } = resizeExact(bitmap, 200, 200, true);
Options
interface CompressionOptions {
maxWidthOrHeight?: number;
width?: number;
height?: number;
keepAspectRatio?: boolean;
rotate?: 0 | 90 | 180 | 270;
mirror?: 'horizontal' | 'vertical';
stripExif?: boolean;
quality?: number;
maxSizeMB?: number;
format?: OutputFormat;
forceServer?: boolean;
forcePath?: CompressionPath;
passThroughUnderBytes?: number;
signal?: AbortSignal;
onProgress?: (progress: CompressionProgress) => void;
}
Types
import type {
CompressionOptions,
CompressionResult,
CompressionProgress,
CompressionError,
DeviceCapabilities,
CompressionPath,
OutputFormat,
DeviceTier,
} from '@gkzlabs/image-compression';
🆚 vs browser-image-compression
The most popular browser compression lib (~6M downloads/month). Here's how
@gkzlabs/image-compression compares:
| Runtime dependencies | 0 (self-contained RPC + Worker) | 1 (uzip) |
| Worker encode path | WebCodecs + OffscreenCanvas (hardware-accelerated) | Canvas2D |
| Output formats | JPEG / WebP / PNG / AVIF (auto-fallback if encoder unsupported) | JPEG / WebP / PNG |
Target file size (maxSizeMB) | ✅ quality + dimension ladder | ✅ quality iteration only |
| HEIC decode | ✅ native ImageDecoder + WASM fallback | ❌ no |
| Transforms (rotate/mirror/exact size) | ✅ main-thread, Chrome-149-safe | partial (EXIF only) |
| Streaming API | ✅ AsyncIterable (no RxJS) | ❌ callback only |
| Cancellation | ✅ AbortSignal | ✅ |
| Batch with bounded concurrency | ✅ compressAll() (anti-OOM) | ❌ manual loop |
| Bundle (main, brotlied) | ~14 KB | ~30 KB+ |
| Framework examples | 5 (Angular/React/Vue/Svelte/Vanilla) | docs only |
TL;DR — same core job, but ours is smaller, zero-dependency, encodes AVIF,
decodes HEIC, exposes a streaming API, and runs the resize/encode on WebCodecs
when available. If you only need a simple JPEG shrink, either works; if you
need format conversion, HEIC support, or hard size guarantees, this library
fits better.
🌐 Browser Support
| Chrome / Edge | 94+ | Best path (WebCodecs + OffscreenCanvas) |
| Safari (macOS) | 16.3+ | OffscreenCanvas + Canvas2D cascade |
| Safari (iOS) | 16.3+ | HEIC native decode (16.4+) |
| Firefox | 105+ | OffscreenCanvas fallback |
| Opera | 80+ | Chromium-based, same as Chrome |
Tier system:
high — Chrome/Edge with WebCodecs + OffscreenCanvas + 4+ cores + 4GB+ RAM
mid — Safari 16.3+ with OffscreenCanvas, 2+ cores
low — Any other browser. Falls back to canvas-main (main thread) or server-fallback
🧪 Tests
npm test
npm run lint
npm run build
Coverage:
service.ts — compress(), compressAll(), cascade logic, error handling
stream.ts — compress$(), compressAll$(), AsyncIterable semantics
types.ts — CompressionError, all union types
capabilities.ts — device feature detection
exif.ts — JPEG EXIF orientation (1-8)
worker-helpers.ts — EXIF auto-rotation, manual applyRotation(), exact resizeExact() (real Canvas2D via @napi-rs/canvas)
transforms.test.ts — 12 tests for rotation, mirror, exact resize, aspect ratio
Skipped tests (7) — require real browser environment:
- 5 tests assume Chrome 149+ environment (run via Playwright e2e)
- 2 tier-downgrade tests require real hardware mocks
🔄 Transform Order
When multiple transforms are specified, they're applied in this order:
1. EXIF auto-rotation (unless rotate is explicitly set)
2. Manual rotate (rotate: 90 | 180 | 270)
3. Mirror (mirror: 'horizontal' | 'vertical')
4. Resize (width/height/maxWidthOrHeight)
5. Encode (format: 'image/jpeg' | 'image/webp' | 'image/png')
🏗️ Architecture
┌─────────────────────────────────────────────────┐
│ ImageCompression (Promise API) │
│ ───────────────────────────── │
│ • getCapabilities() (lazy, cached) │
│ • compress() (single file) │
│ • compressAll() (batched, maxConcurrent) │
└──────────────────┬──────────────────────────────┘
│
┌──────────┴──────────┐
│ │
▼ ▼
┌────────────────┐ ┌─────────────────────────┐
│ compress$() │ │ compressAll$() │
│ ───────────── │ │ ──────────────────── │
│ AsyncIterable │ │ AsyncIterable │
│ Progress + │ │ Per-file progress │
│ Result │ │ + final result array │
└────────────────┘ └─────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 4-path cascade │
│ 1. webcodecs-worker │
│ 2. offscreen-worker │
│ 3. canvas-main │
│ 4. server-fallback │
└─────────────────────────────┘
📂 Project Structure
@gkzlabs/image-compression/
├── src/
│ ├── index.ts # Public API
│ ├── service.ts # ImageCompression class
│ ├── stream.ts # AsyncIterable wrappers
│ ├── types.ts # All types + CompressionError
│ ├── capabilities.ts # detectCapabilities()
│ ├── exif.ts # readExifOrientation()
│ ├── worker.ts # Worker source
│ ├── worker-helpers.ts # EXIF rotation + resize
│ ├── webcodecs.d.ts # Type defs for WebCodecs
│ └── __stubs__/ # Test stubs
├── dist/ # Built output (ESM)
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── tsconfig.test.json
├── vitest.config.ts
├── vitest.setup.ts # Polyfills for tests
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .gitlab-ci.yml
├── LICENSE # MIT
├── CHANGELOG.md
├── README.md
├── CONTRIBUTING.md
└── SECURITY.md
🤝 Related Packages
angular-image-compression — Angular DI wrapper. Adds Observable variants, @Injectable() service. Depends on @gkzlabs/image-compression.
🎮 Live Demo
Try it in your browser — no install needed:
All examples → landing page
📚 Documentation
- Examples Overview — 5 framework examples (vanilla, react, vue, svelte, angular)
- Examples Guide — Detailed framework patterns, lifecycle management, batch processing, HEIC support
- Browser Compatibility — Per-bundler setup notes (Vite, Webpack, Rollup, esbuild)
- API Reference — Generated TypeDoc reference
- Benchmarks — Real-world performance numbers for all 3 cascade paths
⚡ Benchmarks
Run npm run bench to measure webcodecs-worker vs offscreen-worker vs canvas-main on your machine. Latest numbers in the 📊 live dashboard (interactive Chart.js view) or raw BENCHMARKS.md.
📄 License
MIT
🔗 Links