Image compression does not always require a backend. Modern browsers can read files, decode images, draw them onto a Canvas, and encode the result as a Blob. The image can therefore stay on the user's device from selection to download.
This article explains the design behind this site's image compressor. It accepts multiple files, controls output dimensions and quality, reports progress, and provides previews and downloads.
1. Why process images in the browser?
Client-side compression offers two immediate benefits: privacy and speed.
- Images are never uploaded, which is useful for private photos
- No server storage or transfer bandwidth is required
- Processing can begin immediately after file selection
- The tool still works when deployed as a static application
The tradeoff is that the user's device supplies the CPU and memory. Batch jobs should therefore limit concurrency instead of decoding many high-resolution images at once.
2. The processing pipeline
Each image moves through the following pipeline:
Select → validate → decode → calculate dimensions → draw to Canvas
→ encode Blob → create preview URL → download
The tool accepts JPEG, PNG, and WebP. GIF is excluded because drawing it to Canvas preserves only a single frame and removes its animation.
3. Building a multi-file queue
The file input must include the multiple attribute:
<input
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
/>
Every queue entry stores the original File, status, progress, preview URL, output Blob, and image dimensions. Independent item state also means one invalid image cannot stop the rest of the queue.
The implementation detects duplicates using the file name, size, and modification time, and applies a 30 MB limit per image.
4. Calculating output dimensions
Resizing must preserve the original aspect ratio:
const scale = Math.min(
1,
maxWidth / sourceWidth,
maxHeight / sourceHeight,
)
const width = Math.round(sourceWidth * scale)
const height = Math.round(sourceHeight * scale)
Including 1 in Math.min prevents small images from being enlarged. Upscaling cannot create real detail and may produce a larger file.
5. Compressing with Canvas
The browser can decode a file with createImageBitmap and draw it at the target size:
const bitmap = await createImageBitmap(file, {
imageOrientation: "from-image",
})
const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
const context = canvas.getContext("2d")
context?.drawImage(bitmap, 0, 0, width, height)
bitmap.close()
imageOrientation: "from-image" asks the browser to respect the photo orientation. Closing the bitmap after drawing allows its underlying resources to be released earlier.
The Canvas is then encoded with toBlob():
canvas.toBlob((blob) => {
if (!blob) throw new Error("Encode failed")
}, "image/webp", 0.8)
The quality argument works best for JPEG and WebP. PNG is lossless, and browsers generally ignore its quality value. To make a PNG substantially smaller, reduce its dimensions or convert it to WebP.
6. Designing honest progress feedback
Canvas encoding does not expose byte-level progress events, so the UI should not pretend to show exact transfer progress. Each file instead reports its actual stage:
- Waiting
- Reading
- Decoding
- Compressing
- Complete or failed
Overall progress averages the stage progress of every file and also reports the number completed. Files run sequentially to reduce memory pressure on mobile devices.
7. Previewing and downloading
A Blob needs an Object URL before an image element can display it:
const previewUrl = URL.createObjectURL(blob)
Downloading uses a temporary anchor with a download attribute:
const link = document.createElement("a")
link.href = previewUrl
link.download = "photo-compressed.webp"
link.click()
An Object URL keeps its Blob alive. When an image is removed or the component unmounts, release it with:
URL.revokeObjectURL(previewUrl)
Without cleanup, repeatedly adding and removing large images can keep increasing the page's memory use.
8. Format and privacy considerations
Canvas re-encoding normally removes EXIF data, including GPS and camera metadata. This both reduces output size and lowers the risk of accidentally sharing a photo's location. It is not suitable for photography workflows that need to preserve metadata.
Compression also cannot guarantee that every output is smaller. A highly optimized source converted to PNG may grow, so the result list shows the original size, output size, and actual percentage change.
Summary
The File, createImageBitmap, Canvas, Blob, and Object URL APIs are enough to build a practical image optimization workflow without a backend or third-party compression service.
The important part is not merely calling toBlob(). Reliable batch state, isolated errors, honest progress, format differences, and resource cleanup are what make the tool robust in everyday use.