npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gkzlabs/image-compression

v1.3.3

Published

Framework-agnostic image compression for the browser. Zero dependencies — pure web APIs (WebCodecs, OffscreenCanvas, Worker). AVIF/WebP/JPEG output, target-size mode, HEIC decode. Works with any frontend framework (Angular, React, Vue, Svelte) or vanilla

Readme

@gkzlabs/image-compression

npm version npm downloads npm monthly Socket License Zero Dependencies CI Deploy Examples GitHub Pages Bundle Size Tests Provenance

🎮 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.

🚀 Quick Start (3 lines)

import { ImageCompression } from '@gkzlabs/image-compression';

const result = await new ImageCompression().compress(file, { maxWidthOrHeight: 2048, quality: 0.85 });
// → { file, compressedSize, width, height, mimeType, path, ... }

🎬 Want the full experience? Try the live demo (full-featured) or the 5 framework examples.

✨ Features

  • 🚀 4-path cascade — WebCodecs → OffscreenCanvas → Canvas2D → server-fallback
  • 🎯 Target-size modemaxSizeMB guarantees the output fits under a size budget (binary-search quality finds the highest usable quality, then a dimension ladder — v1.1.0 replaced the old fixed quality ladder)
  • 🖼️ AVIF / WebP / JPEG / PNG outputformat: 'image/avif' encodes 30-50% smaller than JPEG, with automatic fallback on browsers that can't encode AVIF
  • qualityBoost — WebP/AVIF quality is raised (+0.1) so photos land near JPEG-at-quality size while looking sharper; low-detail content may grow (see caveat in options)
  • 🪄 sharpen — optional post-resize unsharp-mask (0..1, default 0 = off) to restore edge definition lost during downscaling
  • 🔍 Multi-step downscale — downscaling in ~50% halving steps (same technique as Sharp/Photoshop) preserves noticeably more detail than a single one-shot draw
  • 🔄 Manual rotationrotate: 0 | 90 | 180 | 270 (overrides EXIF auto-rotation) — runs in the Worker since v1.2.0, so transforms on large files don't jank the UI thread
  • 🪞 Mirror/flipmirror: 'horizontal' | 'vertical'
  • 📐 Exact resizewidth / height / keepAspectRatio for precise dimensions
  • 🖼️ Auto EXIF rotation — vertical phone photos auto-orient correctly
  • 🌊 Streaming APIcompress$() and compressAll$() return native AsyncIterable (no RxJS needed)
  • 🖼️ Responsive <picture> in one calltoPictureSet() encodes AVIF/WebP + JPEG fallback, skips formats the engine can't produce, and returns markup + object URLs (v1.3.3)
  • ⚙️ Bounded batch parallelismoptions.maxConcurrency on compressAll() / compressAll$() (v1.3.3; the old positional argument still works)
  • 📦 Framework-agnostic — Zero dependencies on Angular, React, or RxJS
  • 🖼️ HEIC decode — native ImageDecoder first, else the optional heic2any decoder (declared optional peer) or any decoder module via __IC_HEIC2ANY_URL
  • Smart pass-through — Skip compression for already-small JPEGs (passThroughUnderBytes)
  • 🛑 CancellableAbortSignal support for clean cancellation
  • 🧪 Well-tested — 262 unit tests + two real-browser suites (worker thread evidence, pixel checks)
  • 📱 Mobile-friendly — Bounded concurrency (default 2) prevents OOM on phones

📦 Installation

npm install @gkzlabs/image-compression
# or install directly from GitHub
npm install git+ssh://[email protected]/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);      // "photo.jpg"
console.log(result.path);            // "webcodecs-worker" | "offscreen-worker" | "canvas-main" | "server-fallback"
console.log(result.compressedSize);  // bytes

// Cleanup when done
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) {
    // CompressionProgress
    console.log(`[${evt.percent}%] ${evt.stage}`);
  } else {
    // CompressionResult
    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 });
    // Observable variants: this.svc.compress$(file).subscribe(...)
  }
}

📊 API Surface

ImageCompression class

new ImageCompression();
.compress(file: File | Blob, options?: CompressionOptions): Promise<CompressionResult>
.compressAll(files: (File|Blob)[], options?, maxConcurrent?): Promise<(CompressionResult|null)[]>
.getCapabilities(): Promise<DeviceCapabilities>
.terminate(): void   // Stop the Web Worker
.dispose(): void     // Same as terminate (for symmetry with framework lifecycles)

compressAll() return type: with continueOnError: true, failed files appear as null in the returned array (same position as the input) — always null-check each element. With the default continueOnError: false, the batch rejects on the first failure, so a resolved array never contains null.

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';

// Manual rotation (degrees CW) + optional mirror
const { bitmap, width, height } = applyRotation(bitmap, 90, 'horizontal');

// Exact resize (width, height, keepAspectRatio)
const { bitmap, width, height } = resizeExact(bitmap, 800);              // width only
const { bitmap, width, height } = resizeExact(bitmap, undefined, 600);    // height only
const { bitmap, width, height } = resizeExact(bitmap, 200, 200, true);   // fit-within

Options

interface CompressionOptions {
  /** Max width or height — fit-within-box resize (default 2048) */
  maxWidthOrHeight?: number;
  /** Exact target width (overrides maxWidthOrHeight). Height auto if height is omitted */
  width?: number;
  /** Exact target height (overrides maxWidthOrHeight). Width auto if width is omitted */
  height?: number;
  /** When both width+height are set: fit-within-box instead of stretching (default false) */
  keepAspectRatio?: boolean;
  /** Manual rotation in degrees CW: 0 | 90 | 180 | 270. Set 0 to disable EXIF auto-rotation */
  rotate?: 0 | 90 | 180 | 270;
  /** Mirror/flip after rotation: 'horizontal' | 'vertical' */
  mirror?: 'horizontal' | 'vertical';
  /**
   * @deprecated No-op — re-encoding always discards EXIF/XMP/GPS, and this
   * option is not read by the pipeline. Use `passThroughUnderBytes` to keep
   * originals (and their metadata) untouched.
   */
  stripExif?: boolean;
  /** JPEG/WebP/AVIF quality 0..1 (default 0.85) */
  quality?: number;
  /**
   * Target maximum output size in MB — re-encodes until the output fits.
   * v1.1.0: **binary search** finds the highest quality that still fits
   * the budget (instead of the old fixed quality ladder, which overshot
   * and wasted quality), then a dimension ladder (down to 50%). Returns
   * the best result that meets the target, or the smallest achievable
   * with a warning.
   */
  maxSizeMB?: number;
  /** Output format: 'image/jpeg' | 'image/webp' | 'image/png' | 'image/avif' (default 'image/jpeg') */
  format?: OutputFormat;
  /**
   * Post-resize sharpening strength 0..1 (default 0 = off). A light
   * unsharp mask restores edge definition lost during downscaling.
   *   0.2 subtle · 0.5 noticeable · 1 strong
   * Runs on the main thread before encoding (canvas-only; ignored for
   * lossless PNG). See the benchmark feature comparison for cost.
   */
  sharpen?: number;
  /**
   * When the output format is WebP/AVIF, map `quality` up (+0.1, e.g.
   * 0.85 → 0.95). On typical photos (WebP ~30% smaller than JPEG at
   * equal quality) the boosted output lands near the JPEG-at-`quality`
   * size while being visibly sharper. On low-detail content (flat
   * graphics, screenshots) the WebP advantage shrinks and the output
   * can grow 1.5-2× — measure with your own content. Default false.
   */
  qualityBoost?: boolean;
  /** Force server-side processing (skip client compression) */
  forceServer?: boolean;
  /** Force a specific path: 'webcodecs-worker' | 'offscreen-worker' | 'canvas-main' | 'server-fallback' */
  forcePath?: CompressionPath;
  /** Skip compression if file is small + already in target format */
  passThroughUnderBytes?: number;
  /** AbortSignal for cancellation */
  signal?: AbortSignal;
  /** Progress callback */
  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:

| Capability | @gkzlabs/image-compression | browser-image-compression | |---|---|---| | 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.

🎯 Which output format should I use?

Measured on the same 1920×1080 landscape photo (libwebp 1.3 / libaom 3.8):

| Format | Size vs JPEG | Encode speed (4K) | Browser support | |---|---|---|---| | image/jpeg | baseline | 0.12s (fastest) | 100% | | image/webp ⭐ | −28 to −33% | 0.48s (fast) | 99.1% | | image/avif | −63 to −70% | 2.84s+ (24× slower) | 96.4% (decode) |

Recommendation for client-side compression (this library's use case):

  • Use image/webp for uploads. It's ~30% smaller than JPEG with essentially the same encode cost (0.48s vs 0.12s on 4K), and every modern browser supports it. This is the best size/speed trade-off for real-time browser compression.
  • image/jpeg — only when the downstream server/API requires JPEG exactly.
  • image/avif — technically the smallest (−50% vs WebP), but AVIF encode is 24× slower than WebP (2.84s minimum even at max speed on an M2, longer on phones) and still requires a 3MB+ WASM encoder outside Chromium 130+. It shines for server-side / build-time pre-generation, not real-time client-side compression. The library will encode AVIF natively where the browser supports it (Chromium 130+) and transparently fall back to WebP/JPEG elsewhere — but for uploads, WebP is the pragmatic default.
  • image/png — only for lossless / transparency needs (largest output).

🌐 Browser Support

| Browser | Minimum | Notes | |---|---|---| | 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              # 262 passed, 5 skipped, 0 failing
npm run test:coverage # v8 coverage + threshold gate (lines/stmts ≥70, branches ≥70, funcs ≥85)
npm run test:browser  # real Chromium: every cascade path, __IC_WORKER_URL, worker-404 fallback
npm run test:worker   # real Chromium: worker-thread evidence, pixel checks, mid-flight dispose
npm run lint          # tsc clean
npm run build         # ESM + CJS bundle + worker

Coverage:

  • service.tscompress(), compressAll(), cascade logic, error handling
  • stream.tscompress$(), compressAll$(), AsyncIterable semantics
  • types.tsCompressionError, 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(), multi-step downscaleInSteps(), applySharpen() (real Canvas2D via @napi-rs/canvas)
  • quality.spec.ts — v1.1.0 features: multi-step downscale, sharpen, qualityBoost, no-hang guards for 0/NaN/negative targets
  • applyTransforms.spec.ts — 11 tests for rotation, mirror, exact resize, aspect ratio
  • rpc.spec.ts — worker RPC protocol, including the fail-fast path when a worker never loads
  • worker-resolution.spec.ts — the 3 worker-URL strategies (__IC_WORKER_URL, new URL(..., import.meta.url), page-relative fallback)

Skipped tests (5) — need a real browser/hardware:

  • 3 tests assume a Chrome 149+ environment. There is no Playwright/JSDOM e2e suite in CI yet: unit tests run under happy-dom with @napi-rs/canvas as a Canvas2D polyfill, so real Worker / OffscreenCanvas / WebCodecs paths are exercised by npm run bench (headless Chromium via Puppeteer) rather than by npm test. Adding a browser matrix is tracked as the next CI improvement.
  • 2 tier-downgrade tests require real hardware mocks (happy-dom reports no deviceMemory/hardwareConcurrency, so detectCapabilities() returns low)

🔄 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
├── test/                    # Real-browser verification (Puppeteer)
│   ├── browser-smoke.mjs    # all cascade paths + worker-404 fallback
│   └── angular-cli-e2e.mjs  # Angular CLI production build (hostile bundler)
├── examples/                # react, vue, svelte, angular (Vite) + angular-cli
├── 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           # ARCHIVED — GitHub Actions is the only CI
├── 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:

| Framework | Live Demo | Source | | --- | --- | --- | | React | examples/react/ | examples/react/ | | Vue | examples/vue/ | examples/vue/ | | Svelte | examples/svelte/ | examples/svelte/ | | Angular | examples/angular/ | examples/angular/ | | Vanilla | examples/vanilla/ | examples/vanilla/ |

All exampleslanding 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)
  • Server Fallback — How to build the server endpoint for server-fallback results (sharp + Node reference)
  • 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, plus the v1.1.0 feature comparison (cost of sharpen, qualityBoost, multi-step downscale, binary-search target-size — each feature measured on vs off). Latest numbers in the 📊 live dashboard (interactive Chart.js view) or raw BENCHMARKS.md.

📄 License

MIT

🔗 Links