Important note: Canvas-based compression is lossy. It works great for
photos and web assets, but avoid it for pixel-perfect PNGs like UI sprites
or technical diagrams.
What We Are Going to Build
Our script will:
- Accept a File object from an
<input type="file"> - Read the image without uploading it anywhere
- Resize it proportionally to a maximum width
- Fix common EXIF orientation bugs (the "rotated photo" issue)
- Export it as WebP or JPEG with adjustable quality
- Return a compressed Blob ready for upload or download
Step 1: Basic HTML Structure
We start with simple markup for file selection and preview.
html
<input type="file" id="imageInput" accept="image/*" />
<img id="previewImg" alt="Preview" style="max-width:100%" />
Step 2: The Compression Function
async function compressImage(file, maxWidth = 1920, quality = 0.75, mimeType = "image/webp") {
const bitmap = await createImageBitmap(file);
const width = Math.min(bitmap.width, maxWidth);
const height = bitmap.height * (width / bitmap.width);
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext("2d");
ctx.drawImage(bitmap, 0, 0, width, height);
return await offscreen.convertToBlob({
type: mimeType,
quality
});
}
`createImageBitmap()` decodes images off the main thread, which is much faster than `new Image()` for large files.
- `OffscreenCanvas` lets us draw without blocking the page UI.
- `convertToBlob()` is the modern promise-based alternative to the old callback-style `toBlob()`.
- We scale only by width and preserve aspect ratio, so images never stretch.
## Step 3: Wire Up the File Input
Now we listen for file selection, run compression, and show a preview.
const input = document.querySelector("#imageInput");
const preview = document.querySelector("#previewImg");
input.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
console.log("Original size:", (file.size / 1024 / 1024).toFixed(2), "MB");
const compressedBlob = await compressImage(file, 1920, 0.75, "image/webp");
console.log("Compressed size:", (compressedBlob.size / 1024 / 1024).toFixed(2), "MB");
// Create preview URL
const previewUrl = URL.createObjectURL(compressedBlob);
preview.src = previewUrl;
// You can send this blob directly with fetch / axios
// const formData = new FormData();
// formData.append("image", compressedBlob, file.name);
// fetch("/upload", { method: "POST", body: formData })
} catch (err) {
console.error(err);
}
});
## Final Notes
This runs entirely on the user’s device. No image data hits your server until you explicitly send the blob. It is perfect for forms, dashboards, and user upload flows where you want to save bandwidth and storage.
If you just want a ready-to-use tool for quick image compression, check out [Eltorin](https://eltorin.com/).
Top comments (0)