Documentation menu

Client-side image compression before upload

Large phone photos are the single biggest cause of slow uploads. imageCompression re-encodes each image through a canvas before the upload starts — downscaling it and lowering encoder quality — so what crosses the network is a fraction of what the user picked, and your storage bill shrinks with it.

Turn it on with a boolean for sensible defaults:

tsx
import { UpupUploader } from '@useupup/react'
import '@useupup/react/styles'

export default function Uploader() {
    return <UpupUploader uploadEndpoint="/api/upload-token" imageCompression />
}

That alone caps the longest edge at 1920 px and encodes at quality 0.82.

Options

Pass an object (ImageCompressionOptions, exported from @useupup/core/steps/compress) to tune all three knobs:

ts
import { UpupCore } from '@useupup/core'

const core = new UpupCore({
    uploadEndpoint: '/api/upload-token',
    imageCompression: {
        maxWidthOrHeight: 1920,
        maxSizeMB: 1,
        quality: 0.82,
    },
})
  • quality — encoder quality, 01. Default 0.82. Values are clamped into 0.051, and a non-numeric value falls back to the default, so a stray 0 will not produce an unusable file.
  • maxWidthOrHeight — cap on the longest edge, in pixels. Default 1920, applied whether or not you pass the option. Aspect ratio is preserved and images are only ever scaled down; a smaller image is left at its natural size.
  • maxSizeMB — a target output size. No default: without it, the file is encoded exactly once at quality.

The object form is an engine-level option

The visual <UpupUploader> prop imageCompression is typed as a plain boolean toggle. To pass the object, configure it through useUpupUpload or UpupCore — see Headless Usage.

The maxSizeMB backoff

When you set maxSizeMB and the first encode lands over budget, upup retries at progressively lower quality: it subtracts 0.12 each pass and stops at a floor of 0.35. Starting from the default, that is at most five encodes — 0.82 → 0.70 → 0.58 → 0.46 → 0.35. Each pass re-encodes the original image, so quality loss doesn't compound.

maxSizeMB is a best-effort target, not a guarantee. If the file is still over budget at quality 0.35, the loop stops and that result is what gets uploaded. For a hard ceiling, pair it with maxFileSize so oversized files are rejected outright rather than uploaded large.

The trade-off to weigh: a tight maxSizeMB on a large photo can mean five full decode-and-encode cycles per file. Lowering maxWidthOrHeight usually reaches the same byte target in one pass, because pixels shed size faster than quality does.

Output format

The encoder preserves image/png and image/webp; every other input type comes out as image/jpeg. The filename is unchanged, so a still .gif compressed to JPEG bytes keeps its original name — set the storage key or rename the file yourself if the extension matters to you. (An animated GIF is not compressed at all — see below.)

When compression does nothing

Compression can decide to keep the original file. Three cases:

  • The image is animated. A canvas has no animated encoder, so re-encoding an animated GIF, animated WebP or APNG would upload its first frame and throw the rest away. upup detects those from the file's bytes and skips the step entirely — the original uploads untouched, with no compressed metadata. Still images of the same formats are compressed normally. stripExifData skips them for the same reason, so an animated image reaches your storage exactly as the user picked it, EXIF included.
  • No size benefit. If the re-encoded result is not smaller than the original, upup keeps the original — but only when you passed neither maxSizeMB nor an explicit maxWidthOrHeight. Setting either of those is read as an instruction to normalize the image, so the re-encoded output is kept even if it grew.
  • No canvas backend. In an environment where the image cannot be decoded at all — server-side rendering, a runtime with no canvas — the step returns the file untouched. It is a silent no-op: no pipeline-error event is emitted for compression, unlike HEIC conversion.

Metadata a compressed file carries

When a file is actually compressed, upup stamps its metadata with originalSize, processedSize, width, height, and compressed: true. width and height are the dimensions of the encoded output, so they are also the cheapest way to confirm a downscale happened:

ts
core.on('upload-all-complete', files => {
    for (const file of files) {
        const { originalSize, processedSize, width, height } =
            file.metadata ?? {}
        const size = `${originalSize} → ${processedSize}`
        console.log(file.name, size, `${width}×${height}`)
    }
})

How it interacts with the rest of the pipeline

  • HEIC runs first. A .heic photo is converted to JPEG before compression sees it, so compression works on decodable bytes. See HEIC to JPEG.
  • EXIF stripping runs before compression. Enabling both means two encodes per image. Compression's own re-encode drops EXIF as a side effect, but only when it actually replaces the file — an image that lands in the "no size benefit" case above keeps its original bytes, metadata included. If GPS coordinates must never survive, enable stripExifData rather than relying on compression.
  • Hashing runs after. With checksumVerification on, the checksum describes the compressed bytes, not the original.
  • Thumbnails are separate. thumbnailGenerator annotates the file with a preview data URL and does not affect the uploaded bytes.

Compression also runs on a Web Worker whenever one is available, and falls back to the main thread on timeout or failure without failing the file — see Web Worker offload.

Observing it

pipeline-step fires with step: 'compress' after the step runs on a file, and pipeline-start lists compress among the steps that will run:

ts
core.on('pipeline-start', ({ fileId, steps }) => {
    console.log(fileId, steps) // e.g. ['exif', 'compress', 'hash']
})

A file skipped by the step's shouldProcess filter — anything whose MIME type isn't image/* — fires no pipeline-step at all.

  • File Processing — the pipeline overview and the fixed step order.
  • Optional props — every <UpupUploader> prop, including maxFileSize and the other processing toggles.
  • Eventspipeline-start, pipeline-step, pipeline-complete, and pipeline-error payloads.