How to Compress an Image
Image compression reduces file size so pages load faster, storage costs less, and bandwidth bills shrink. This guide explains how compression works, which format to choose, and how to implement it in JavaScript, Python, Go, and PHP.
Lossy vs Lossless
All image compression falls into one of two categories:
| Type | How it works | File size reduction | Quality impact | Best for |
|---|---|---|---|---|
| Lossy | Discards data the eye barely notices (colour detail, high-frequency edges) | 50–90% smaller | Slight; visible at low quality | Photos, hero images, thumbnails |
| Lossless | Re-encodes data with no information loss (run-length encoding, deflate) | 10–30% smaller | None | Screenshots, logos, icons, line art |
The key parameter for lossy compression is quality — a number from 0 (worst) to 100 (best). A quality of 80 typically cuts JPEG file size in half with no perceptible difference on screen.
Format Quick-Reference
| Format | Compression | Transparency | Animation | When to use |
|---|---|---|---|---|
| JPEG | Lossy | ✗ | ✗ | Photos, gradients |
| PNG | Lossless | ✓ | ✗ | Logos, screenshots, transparent graphics |
| WebP | Both | ✓ | ✓ | Web — 25–35% smaller than JPEG/PNG at the same quality |
| AVIF | Both | ✓ | ✓ | Web — best compression ratio, slower encode |
| GIF | Lossless (256 colours) | ✓ (1-bit) | ✓ | Simple animations only |
Rule of thumb: convert photos to WebP (or JPEG fallback); convert transparent graphics to WebP (or PNG fallback).
JavaScript — Sharp (Node.js)
Sharp is the fastest Node.js image library, built on libvips.
import sharp from "sharp";
// Lossy JPEG at quality 80
await sharp("input.jpg")
.jpeg({ quality: 80, mozjpeg: true })
.toFile("output.jpg");
// Lossless PNG optimisation
await sharp("input.png")
.png({ compressionLevel: 9, palette: true })
.toFile("output.png");
// Convert to WebP — best bang-for-buck on the web
await sharp("input.jpg")
.webp({ quality: 82 })
.toFile("output.webp");
// Resize + compress in one pass (most common pipeline)
await sharp("input.jpg")
.resize({ width: 1200, withoutEnlargement: true })
.webp({ quality: 80 })
.toFile("output.webp");
Browser — Canvas API (client-side):
function compressImage(file, quality = 0.8, maxWidth = 1920) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const scale = Math.min(1, maxWidth / img.width);
const canvas = document.createElement("canvas");
canvas.width = img.width * scale;
canvas.height = img.height * scale;
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(resolve, "image/webp", quality);
};
img.src = URL.createObjectURL(file);
});
}
// Usage
const compressed = await compressImage(inputFile, 0.82);
canvas.toBlobwith"image/webp"is supported in all modern browsers. Provide a JPEG fallback for Safari < 14.
Python — Pillow
from PIL import Image
import os
def compress_image(input_path: str, output_path: str, quality: int = 80) -> None:
img = Image.open(input_path)
# Convert RGBA → RGB for JPEG (JPEG has no alpha channel)
if img.mode == "RGBA" and output_path.lower().endswith((".jpg", ".jpeg")):
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
ext = os.path.splitext(output_path)[1].lower()
if ext in (".jpg", ".jpeg"):
img.save(output_path, "JPEG", quality=quality, optimize=True, progressive=True)
elif ext == ".png":
img.save(output_path, "PNG", optimize=True)
elif ext == ".webp":
img.save(output_path, "WEBP", quality=quality, method=6)
else:
img.save(output_path, optimize=True)
# Example: compress with resize
def compress_and_resize(src: str, dst: str, max_width: int = 1200, quality: int = 80) -> None:
img = Image.open(src)
if img.width > max_width:
ratio = max_width / img.width
img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS)
compress_image_obj(img, dst, quality)
def compress_image_obj(img: Image.Image, dst: str, quality: int) -> None:
ext = dst.rsplit(".", 1)[-1].lower()
fmt = {"jpg": "JPEG", "jpeg": "JPEG", "png": "PNG", "webp": "WEBP"}.get(ext, "JPEG")
img.save(dst, fmt, quality=quality, optimize=True)
Go — imaging + jpeg package
Go's standard library has no image compression beyond PNG (deflate). Use the disintegration/imaging package for resizing and jpeg for quality control:
package main
import (
"image/jpeg"
"image/png"
"os"
"github.com/disintegration/imaging"
"golang.org/x/image/webp" // decode only; encode via external tool
)
// CompressJPEG saves src as JPEG at the given quality (1–100).
func CompressJPEG(inputPath, outputPath string, quality int) error {
img, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
if err != nil {
return err
}
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
return jpeg.Encode(out, img, &jpeg.Options{Quality: quality})
}
// ResizeAndCompress resizes to maxWidth then compresses.
func ResizeAndCompress(inputPath, outputPath string, maxWidth, quality int) error {
img, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
if err != nil {
return err
}
if img.Bounds().Dx() > maxWidth {
img = imaging.Resize(img, maxWidth, 0, imaging.Lanczos)
}
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
return jpeg.Encode(out, img, &jpeg.Options{Quality: quality})
}
For WebP output in Go, call
cwebpviaexec.Commandor use thechai2010/webppackage.
PHP — GD and Imagick
GD (built into most PHP installs):
function compressImage(
string $inputPath,
string $outputPath,
int $quality = 80
): void {
$info = getimagesize($inputPath);
$mime = $info['mime'];
$src = match ($mime) {
'image/jpeg' => imagecreatefromjpeg($inputPath),
'image/png' => imagecreatefrompng($inputPath),
'image/webp' => imagecreatefromwebp($inputPath),
'image/gif' => imagecreatefromgif($inputPath),
default => throw new \InvalidArgumentException("Unsupported format: $mime"),
};
$ext = strtolower(pathinfo($outputPath, PATHINFO_EXTENSION));
match ($ext) {
'jpg', 'jpeg' => imagejpeg($src, $outputPath, $quality),
'png' => imagepng($src, $outputPath, (int) round(9 - $quality / 11)),
'webp' => imagewebp($src, $outputPath, $quality),
'gif' => imagegif($src, $outputPath),
default => throw new \InvalidArgumentException("Unsupported output: $ext"),
};
imagedestroy($src);
}
PNG quality in
imagepngruns 0–9 (deflate level), not 0–100. The formularound(9 - quality / 11)maps a 0–100 quality input to the right deflate level.
Imagick (better quality, requires the extension):
function compressImageImagick(
string $inputPath,
string $outputPath,
int $quality = 80
): void {
$imagick = new \Imagick($inputPath);
$imagick->autoOrient();
$imagick->setImageCompressionQuality($quality);
$imagick->stripImage(); // remove EXIF/ICC metadata for smaller file
$ext = strtolower(pathinfo($outputPath, PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg'])) {
$imagick->setImageFormat('jpeg');
} elseif ($ext === 'webp') {
$imagick->setImageFormat('webp');
}
$imagick->writeImage($outputPath);
$imagick->destroy();
}
Typical File Size Reductions
| Input | Method | Before | After | Savings |
|---|---|---|---|---|
| 4 MP photo, JPEG q=95 | Re-encode JPEG q=80 | 3.2 MB | 800 KB | 75% |
| 4 MP photo, JPEG q=80 | Convert to WebP q=80 | 800 KB | 560 KB | 30% |
| PNG screenshot (1920×1080) | PNG optimize | 1.4 MB | 900 KB | 36% |
| PNG screenshot (1920×1080) | Convert to WebP | 1.4 MB | 200 KB | 86% |
| PNG logo with transparency | WebP lossless | 120 KB | 75 KB | 37% |
6 Common Pitfalls
1. Re-compressing an already-compressed JPEG Each lossy save applies compression on top of prior compression. Artefacts accumulate. Always start from the highest-quality source available; never chain JPEG → JPEG → JPEG.
2. Stripping alpha when converting to JPEG JPEG has no transparency. Converting RGBA PNG directly produces black or corrupted backgrounds. Flatten against a white (or brand-colour) background first.
3. Using quality=100 for "lossless" JPEG
JPEG at quality 100 is still lossy — it just retains nearly all data. Use PNG or WebP lossless if you need bit-perfect preservation.
4. Ignoring EXIF orientation
Phones embed rotation data in EXIF rather than rotating pixels. Compressing without reading EXIF can produce sideways thumbnails. Call autoOrient (Imagick), AutoOrientation(true) (imaging), or ImageOps.exif_transpose (Pillow) before encoding.
5. Measuring savings on tiny images Format overhead (headers, colour tables, ICC profiles) dominates below ~10 KB. Don't benchmark compression on small images; run it on representative full-size inputs.
6. Compressing images that are already small If an image is already 50 KB, compressing to 40 KB saves 10 KB but adds latency for on-the-fly processing. Set a minimum file size threshold (e.g., skip files < 100 KB).
FAQ
What quality setting should I use for JPEG? 80–85 for most web images. Drop to 70–75 for thumbnails. The difference between 80 and 85 is invisible at typical screen sizes; the file-size difference is 15–20%.
Is WebP always better than JPEG? For photos, WebP is typically 25–35% smaller at equivalent visual quality. The exception: very small images (< 5 KB) where JPEG format overhead is proportionally lower. Support is 97%+ of browsers as of 2025.
How do I compress an image without losing quality?
Use lossless compression: PNG with optimize=True, WebP lossless, or AVIF lossless. Savings are smaller (10–30%) but pixel-perfect.
Should I compress images at upload time or at request time?
Both: compress at upload to store the smallest master, then serve responsive variants (different sizes/formats) from a CDN at request time using srcset and <picture>.
What's the difference between image compression and image resizing? Resizing changes the pixel dimensions; compression reduces the file size of pixels that remain. Both reduce file size — resizing is often more effective for oversized images (a 4000 × 3000 photo served at 800 × 600 is four times the data you need).
Can I compress images in the browser before upload?
Yes — use Canvas API with toBlob (see the JavaScript section). This reduces server bandwidth and upload time. Be aware that toBlob re-encodes from the decoded pixel buffer, so quality settings apply on top of any prior encoding.