Build a browser image resizer — no server, no upload
You can resize an image entirely in the browser: the file never leaves the user's device, there's no server to run, and it works offline. This tutorial walks through exactly how our Image Resizer tool is built, one primitive at a time, so you can build your own. The whole thing is a short pipeline of standard Web APIs — read a file, decode it, draw it smaller on a <canvas>, and hand the result back as a downloadable file.
The whole pipeline in one line
Every browser image tool — resize, compress, convert, crop — is the same four steps:
File → <img> / ImageBitmap → <canvas> (draw at new size) → Blob → downloadNothing here needs a library or a network request. Let's build each arrow.
1 · Read the file without uploading it
Give the user a file input (and, nicely, a drag‑and‑drop zone). The File object you get back is already the raw bytes in memory — reading it is a local operation, not an upload.1
<input type="file" id="file" accept="image/*" hidden>
<div id="drop">Drop an image, or click to choose</div>To turn those bytes into something drawable, wrap the file in an object URL — a short blob: link to the in‑memory data — and load it into an Image. createObjectURL() is instant and never copies the file anywhere.2
const img = new Image();
function loadFile(file) {
if (!file.type.startsWith("image/")) return; // ignore non-images
const url = URL.createObjectURL(file); // blob: link to the bytes
img.onload = () => {
// img.naturalWidth / naturalHeight are now the real pixel size
setupControls(img.naturalWidth, img.naturalHeight);
};
img.src = url; // decode happens here
}Wire it to both the input and the drop zone. A drop is just a drop event whose dataTransfer.files holds the same File objects the input would give you — so both paths call the one loadFile().
2 · Draw it at a new size on a canvas
The resize itself is a single call. Create a <canvas> at the target dimensions and let drawImage() paint the source image into it — the five‑argument form drawImage(image, dx, dy, dWidth, dHeight) scales as it draws.3
function resizeTo(width, height, type, quality) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high"; // ask for the best resampling
ctx.drawImage(img, 0, 0, width, height); // scale into the canvas
return canvas;
}Two lines matter for quality. The browser interpolates when it scales, and imageSmoothingQuality = "high" requests its best resampling filter rather than a fast‑and‑blocky one.4 For large downscales, one clean step like this generally beats naïve pixel‑dropping; if you ever need extreme reductions, downscaling in halves is a known trick, but for typical photo work a single high‑quality draw is enough.
3 · Keep the aspect ratio
If the user edits the width, recompute the height from the original ratio (and vice‑versa) so nothing stretches. Store the ratio once when the image loads:
const ratio = img.naturalWidth / img.naturalHeight;
widthInput.addEventListener("input", () => {
if (lockAspect.checked) {
heightInput.value = Math.round(widthInput.value / ratio);
}
});
// scale presets are just: w = round(naturalWidth * 0.5), etc.That's the entire "lock aspect ratio" feature. Turning the lock off simply skips the recompute, letting the user squash or stretch on purpose.
4 · Export: format and quality
Read the canvas back out as a compressed file with toBlob(). It's asynchronous — you get the finished Blob in a callback — and it takes the MIME type and, for lossy formats, a quality between 0 and 1.5
canvas.toBlob(
(blob) => { /* blob is the encoded image file */ },
"image/webp", // or "image/jpeg", "image/png"
0.9 // quality 0–1 (ignored for PNG)
);The quality argument only applies to lossy encoders — JPEG and WebP; it is meaningless for lossless PNG, which is why our tool hides the slider when PNG is selected.6 Format choice is real: WebP typically lands 25–35% smaller than JPEG at comparable quality, so it's the best default for photos on the web; keep PNG for logos, screenshots, and anything with transparency.7 One JPEG caveat — it has no alpha channel, so paint a solid background before drawing if your source might be transparent:
if (type === "image/jpeg") {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height); // otherwise transparency turns black
}
ctx.drawImage(img, 0, 0, width, height);5 · Hand back the download
A Blob becomes a saved file the same way the input became an image: wrap it in an object URL and click a hidden anchor carrying the download attribute, which names the file.8
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "resized-" + width + "x" + height + ".webp";
a.click();
setTimeout(() => URL.revokeObjectURL(url), 4000); // free the memory
}, "image/webp", 0.9);That revokeObjectURL() matters: object URLs pin their data in memory until you release them, so revoke every one you create once you're done with it.9 Do the same for the source URL when the user loads a new file.
The gotchas nobody mentions
EXIF orientation. Phone photos often carry an "orientation" flag instead of physically rotating the pixels. Plain drawImage ignores it, so a portrait shot can come out sideways. The fix is to decode with createImageBitmap(file, { imageOrientation: "from-image" }), which bakes the rotation in before you draw.10
Tainted canvases. If you ever draw an image loaded cross‑origin without permission, the canvas becomes "tainted" and toBlob throws a security error. Reading a user's own File (as here) is same‑origin data, so it's safe — but it's the first thing to check if export suddenly fails.11
Memory. Very large images are large canvases; free object URLs promptly and don't hold references to old canvases. Feature support: WebP encoding via toBlob is widely available now, but a defensive tool checks that blob isn't null and falls back to JPEG if a browser can't produce the requested type.
Why do it in the browser at all?
Because the alternative — upload, resize server‑side, download — costs you a server, bandwidth, and your users' trust. The client‑side version is private by construction (the bytes never leave the device), free to run, works offline, and is instant with no round trip. For resizing, compression, format conversion, cropping and watermarking, the canvas is all you need. Our whole tools collection is built on this principle: the useful work happens on your machine, and nothing is uploaded.