I'm Kamal, the developer behind OnlineToolkitHub, a site with 250+ free browser tools. This post is about how one of its workflows, Resize, Compress & Convert Images, works under the hood.
Anyone who has built or run a website knows the routine. A client sends twenty photos straight off their phone. Each one is 4032×3024 pixels and 3–5 MB. Before they go anywhere near a page, every single file needs the same three jobs: shrink it, compress it, and convert it to something lighter than JPEG.
Most "compress image online" sites handle this by uploading your files to a server, processing them there and sending them back. That works, but it means handing your photos to a stranger's server, and phone photos carry more than pixels: the camera model, the time they were taken and often the exact GPS location.
None of this work needs a server. A browser can decode an image, resize it, and re-encode it as WebP on its own. So the workflow does everything inside the tab.
The shape of the tool
The UI is deliberately small and follows the three jobs in order:
- Add images. Drop in as many JPG, PNG or WebP files as you like.
- Resize and compress. Set one number (the longest side in pixels), pick a format (WebP, JPEG or PNG) and a quality value.
- Download. Check the before/after sizes and download one file or all of them.
There's no sign-up, no watermark and no daily limit. One set of settings applies to the whole batch, because that's how people actually prepare images for a site: every photo in a blog post or product gallery wants the same treatment.
The defaults are a longest side of 1600 px, WebP and quality 80. That's a sensible starting point for most sites: big enough for a full-width image on a laptop, small enough not to hurt page speed.
One number for resizing: the longest side
Asking for "width and height" is a trap for batch work. A batch usually mixes landscape and portrait photos, and a fixed width would make the portrait shots enormous. So the tool asks for a single value, the longest side, and scales each image so that its longer edge fits:
// Simplified: fit the longest side, never upscale.
function targetSize(w, h, maxSide) {
const scale = Math.min(1, maxSide / Math.max(w, h));
return {
width: Math.round(w * scale),
height: Math.round(h * scale),
};
}
The Math.min(1, …) matters. An image that's already smaller than the limit is left at its original size, because scaling a small image up only adds pixels the camera never captured. You can see this in the results further down: a 4032×3024 landscape becomes 1600×1200, a 3024×4032 portrait becomes 1200×1600, and a 1200×800 thumbnail stays 1200×800.
Re-encoding with a canvas
Once the target size is known, the image is drawn onto a canvas at that size and the canvas is exported in the chosen format. The core of it looks roughly like this:
// Simplified sketch of the per-file pipeline.
async function processImage(file, { maxSide, type, quality }) {
const img = await loadImage(file); // decode locally via an object URL
const { width, height } = targetSize(img.naturalWidth, img.naturalHeight, maxSide);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingQuality = "high";
ctx.drawImage(img, 0, 0, width, height);
// type: "image/webp" | "image/jpeg" | "image/png"
// quality: 0..1, ignored by the browser for PNG
return new Promise((resolve) =>
canvas.toBlob(resolve, type, quality / 100)
);
}
canvas.toBlob() uses the browser's own encoders, so there's no image library to download. WebP support is built into every current browser, and WebP is typically 25–35% smaller than a JPEG at the same visual quality, which is why it's the default.
The result of each file is a Blob. The preview thumbnails and download buttons point at blob: URLs created from those blobs, which only exist inside your tab. When you clear the batch, the URLs are revoked with URL.revokeObjectURL() so the memory is released.
What the numbers looked like
For this post I ran a batch of four test JPEGs through the defaults (1600 px, WebP, quality 80):
| File | Before | After |
|---|---|---|
| IMG_2041 | 4032×3024 · 1.70 MB | 1600×1200 · 126.3 KB |
| IMG_2042 | 3024×4032 · 1.75 MB | 1200×1600 · 134.4 KB |
| banner-hero | 4000×2250 · 1.48 MB | 1600×900 · 112.0 KB |
| thumb-small | 1200×800 · 237.9 KB | 1200×800 · 58.4 KB |
| Total | 5.17 MB | 431.1 KB (92% smaller) |
Your numbers will depend on the photos. Real camera images with lots of fine detail compress less than flat graphics, but a 3–5 MB phone photo usually lands at a few hundred KB, with no difference you'd notice on screen. Most of the saving comes from the resize: going from 12 megapixels to about 2 cuts the pixel count by roughly 85% before compression even starts.
What doesn't survive
It's better to be upfront about what re-encoding through a canvas does and doesn't keep:
- EXIF metadata is dropped. A canvas holds pixels, not metadata, so camera details, timestamps and GPS coordinates don't make it into the output. For the web that's usually exactly what you want, but keep your originals if you need that data. (If you only want to remove metadata without touching anything else, there's a separate Strip Image Metadata tool on the site.)
- Animated GIFs become a single still frame. Drawing a GIF onto a canvas captures only its current frame.
- Quality doesn't affect PNG. PNG is lossless, so the browser ignores the quality argument. PNGs only get smaller through resizing. If a PNG is still too big, export as WebP instead, which also keeps transparency.
- The output is a fresh encode, not the original bytes. Unlike lossless operations, this is a lossy conversion by design. Don't run the same image through it repeatedly; always start from the original.
Memory is the real limit
There's no fixed cap on how many images you can add, but everything happens in your browser's memory. A decoded 12-megapixel photo takes around 48 MB as raw pixels while it's being drawn, regardless of how small the JPEG file was. Decoding a whole batch at the same time multiplies that peak, which is what can freeze or crash a tab on a phone.
In practice, a few dozen phone photos is fine on most laptops. For hundreds, work in smaller batches.
Checking that nothing gets uploaded
"Your photos aren't uploaded" is a promise, and you shouldn't have to take my word for it. You can check it yourself in about a minute:
- Open the workflow page.
- Open DevTools (F12 or Cmd+Option+I) and go to the Network tab.
- Add a few images and click Process images.
When I did this while writing the post, the requests during processing were:
-
blob:GETs. These are the browser reading the files you picked and showing previews. They never leave the tab. - Analytics. The site uses Google Analytics and Microsoft Clarity for anonymous usage stats. The analytics event for this workflow records that a batch was completed and how many images were in it. It doesn't include file names or image data.
There's no request that sends your images anywhere, because there's no server endpoint for this workflow to send them to. You still need an internet connection to load the page itself.
Just need one step?
The workflow chains three jobs, but each one also exists as its own tool with a few extra options:
- Resize Image: just the resize step.
- Compress Image: just compression (75–85 is the usual sweet spot for photos).
- JPG to WebP Converter: format conversion only, no resizing.
Wrapping up
The whole workflow is a single page, a resize calculation, one drawImage call and one toBlob call per file. It's free, there's no account, and your photos stay on your device.
Try it here: Resize, Compress & Convert Images
If you're working on something similar, I'd like to hear how you handle EXIF orientation on older browsers, or whether you've had better results with a WASM encoder like MozJPEG or libwebp than with the browser's built-in ones. That's the next thing I'd like to look at.
Top comments (0)