@nitida/asset-compressor-web
v0.6.4
Published
nitida browser image compression — a worker on OffscreenCanvas, so the main thread never blocks. HEIC decodes natively where the engine can; the WASM decoder is the fallback, not the default.
Maintainers
Readme
@nitida/asset-compressor-web
Browser image compression for the nitida upload
pipeline. Decode, resize and re-encode happen on an OffscreenCanvas inside a
Web Worker, so the main thread never blocks while a batch of photos is
processed. Pairs with @nitida/asset-uploader-web and mirrors the API of
@nitida/asset-compressor-native so call sites are identical across platforms.
Install
npm install @nitida/asset-compressor-webNo peer dependencies. compressorjs and heic2any ship as dependencies; the
HEIC decoder is imported lazily and only on the browsers that need it.
Usage
import { compressImage, compressImages } from "@nitida/asset-compressor-web";
// Single file
const result = await compressImage({
blob: file,
filename: file.name,
statusCallback: (status) => console.log(status), // "compressing" | "convertingHeic" | "skipped" | "done"
});
// → { blob, filename, originalSize, compressedSize, compressionRatio, originalArrayIndex }
// Batch — concurrency adapts to navigator.hardwareConcurrency, capped at 8
const { successful, failed } = await compressImages(
files.map((f, i) => ({ blob: f, filename: f.name, id: String(i) })),
{ quality: 0.85, maxWidth: 2880 },
(id, status) => console.log(`${id} → ${status}`),
);compressImages never rejects on a single bad file: failures land in failed
with the original blob attached, so a 40-photo batch is not lost to one
unreadable image.
Four behaviours that surprise people
The output is WebP even when the input was JPEG. mimeType defaults to
image/webp for every image, and convertSize is pinned to 0 so the
compressorjs gate that normally skips files under 5 MB never fires. WebP is
~30 % smaller than JPEG at the same quality and ~70 % smaller than PNG for
photos, and the server generates WebP variants anyway. Pass
{ mimeType: "image/png" } when a format must be preserved exactly — an icon
whose alpha channel matters, for instance.
Images above 40 megapixels come back uncompressed, by design. A very large
render can exceed the browser's canvas-area limits — mobile Safari most
aggressively — and produce a silently blank result. Above the ceiling the
original is passed through untouched for the server to resize with sharp, so a
compressionRatio of 1 on a 96 MP file is the guard working, not a failure.
HEIC decodes natively first. WebKit decodes HEIC itself, and WebKit is what
iPhones run — the device that produces essentially every HEIC you will ever
receive. The heic2any WASM decoder is 341 kB gzipped and is the fallback, not
the plan: it loads only when the engine cannot decode the file. If you see
convertingHeic on iOS, something is wrong.
A blank result is caught, not shipped. Every worker output below the
megapixel ceiling is checked for blank pixels and decode crashes before it is
returned; a file that fails falls back to the main-thread compressorjs path.
That fallback is also what runs on Safari < 16.4, which has no
OffscreenCanvas.
Defaults
| option | default | note |
| --- | --- | --- |
| quality | 0.85 | visually lossless for product photos |
| maxWidth / maxHeight | 2880 | longest side of the output |
| mimeType | image/webp | regardless of source format |
| convertSize | 0 | the skip-small-files gate is deliberately disabled |
keepOriginalDimensions: true ignores the dimension caps;
skipCompression: true bypasses the pipeline entirely and returns the source.
Also exported
isHeic, canDecodeHeicNatively, readImageDimensions, computeResizeTarget,
isOffscreenCompressionSupported, probeCanvasEncoder,
getOptimalCpuConcurrency, limitConcurrency, and the
DEFAULT_COMPRESSION_OPTIONS / MAX_UPLOAD_DIMENSION /
MAX_CLIENT_RENDER_MEGAPIXELS constants — useful when a UI needs to show what
will happen before it happens.
License
MIT
