jpegoptim is the small CLI tool I reach for whenever a directory full of JPEGs is too heavy to ship. It runs lossless by default (just rewrites the Huffman table and strips optional markers), then offers --max=N for lossy recompression and --strip-all for metadata removal. The bulk pattern is find ... -print0 | xargs -0 -P 8 jpegoptim --strip-all --max=85, which parallelizes across CPU cores and handles tens of thousands of images cleanly. This page is the reference I keep open while writing image pipelines: install on macOS/Linux/Windows, every flag I actually use, the parallel pattern, and how jpegoptim compares to mozjpeg, squoosh-cli, sharp-cli, and the WebP/AVIF transcoding options.
How do I optimize JPEG images with jpegoptim?
Install jpegoptim with brew install jpegoptim on macOS, apt install jpegoptim on Debian/Ubuntu, or download a Windows binary from the project's release archive (WSL also works). The simplest invocation is jpegoptim image.jpg, which rewrites the file in place with a smaller lossless version. For lossy compression, add --max=85 to cap quality at 85 percent (typical web-quality threshold), and --strip-all to remove EXIF, ICC, and other metadata. To process a folder in parallel, use find . -name '*.jpg' -print0 | xargs -0 -P 8 jpegoptim --strip-all --max=85: the -P 8 runs 8 jobs at once. Always back up originals first or use --dest=/path/to/output to write optimized copies to a separate folder. For the underlying loop syntax, see Bash For Loops.
Jump to:
- Install jpegoptim
- Lossless vs lossy compression
- Strip metadata with --strip-all
- Bulk processing with find + xargs -P
- jpegoptim vs mozjpeg vs guetzli
- jpegoptim vs optipng (don't mix them)
- WebP and AVIF: when to skip JPEG entirely
- Common pitfalls
- What to do next
- FAQ
Install jpegoptim
macOS (Homebrew):
brew install jpegoptimDebian / Ubuntu:
sudo apt update
sudo apt install jpegoptimFedora / RHEL:
sudo dnf install jpegoptimAlpine (used in many Docker images):
apk add jpegoptimWindows: there's no official native installer. Two practical options: (1) install WSL (Windows Subsystem for Linux) and use the Ubuntu path above, (2) download a pre-built Windows binary from the jpegoptim GitHub releases and drop jpegoptim.exe somewhere on your PATH.
Confirm the install:
jpegoptim --versionExpect output like jpegoptim v1.5.5 Copyright (C) Timo Kokkonen, 1996-2024.
Lossless vs lossy compression
By default, jpegoptim is lossless. It re-encodes the Huffman table to be optimal for the image's data, strips optional JPEG markers, and rewrites the file. The pixel data is byte-for-byte identical after decode. Typical savings: 5 to 15 percent. If jpegoptim can't make the file smaller, it leaves the original alone (you can override with --force).
jpegoptim image.jpg
# image.jpg 2400x1600 24bit Exif IPTC ICC JFIF [OK] 480123 --> 421567 bytes (12.19%), optimized.For lossy compression, pass --max=N where N is the maximum allowed JPEG quality (0 to 100):
jpegoptim --max=85 image.jpgIf the image was already saved at quality 75, jpegoptim leaves it untouched (it won't raise quality). If the image was saved at quality 95, jpegoptim recompresses it down to 85. Common web targets: 85 for hero images, 75 for thumbnails, 60 for blur-up placeholders.
You can also target a specific file size with --size=N:
jpegoptim --size=150k image.jpg
jpegoptim --size=80% image.jpgThe size form is convenient for hitting a hard budget (e.g., Twitter card max), but the resulting quality is variable and unpredictable across images. For consistent visual quality across a batch, --max=N is the right knob.
Strip metadata with --strip-all
Cameras embed EXIF data with GPS coordinates, camera serial numbers, and timestamps. Phones embed even more. For web images this is usually unwanted: privacy risk (location leak) plus 4 to 30 KB of pure overhead per image.
jpegoptim --strip-all image.jpg--strip-all removes every optional marker. For more surgical control:
| Flag | Removes |
|---|---|
--strip-all | every optional marker |
--strip-exif | EXIF only (GPS, camera info, timestamps) |
--strip-iptc | IPTC (caption, byline, copyright) |
--strip-icc | ICC color profiles |
--strip-xmp | XMP metadata |
--strip-com | comment markers |
--strip-jfif | JFIF (rarely needed) |
Keeping the ICC profile is sometimes correct: if your images use a wide-gamut color space (e.g., Display P3 from an iPhone), stripping ICC causes browsers to render them as sRGB and the colors look wrong. For photography sites, run jpegoptim --strip-exif --strip-iptc --strip-xmp --strip-com to keep ICC but drop everything else.
Bulk processing with find + xargs -P
The canonical pattern for "optimize every JPEG in a folder tree":
find . -type f \( -iname '*.jpg' -o -iname '*.jpeg' \) -print0 \
| xargs -0 -P 8 -n 50 jpegoptim --strip-all --max=85Breaking it down:
find . -type frecurses into all subdirectories looking for files only.-iname '*.jpg' -o -iname '*.jpeg'matches both extensions, case-insensitive.-print0separates results by NUL bytes, so filenames with spaces or newlines work safely.xargs -0reads NUL-separated input.-P 8runs up to 8 jpegoptim processes in parallel.-n 50passes up to 50 filenames per invocation, which amortizes the process-startup cost.
-P 8 is a sensible default for an 8-core laptop. On a 32-core build machine, raise it to 16 or 24 (jpegoptim is single-threaded but disk IO and CPU caches limit benefit past about 2x cores). On a Raspberry Pi or shared host, set it to 2 or 4.
To process files smaller than a threshold (skip already-tiny thumbnails):
find . -type f -iname '*.jpg' -size +50k -print0 \
| xargs -0 -P 8 jpegoptim --strip-all --max=85-size +50k matches files larger than 50 KB. For more find patterns, see the find Command Cheat Sheet.
To preserve originals and write optimized copies to a parallel directory tree, use --dest:
mkdir -p /var/optimized
jpegoptim --strip-all --max=85 --dest=/var/optimized image.jpgThe --dest directory must exist beforehand; jpegoptim won't create it.
For loop-based batch with logging, see the Bash For Loops reference.
jpegoptim vs mozjpeg vs guetzli
jpegoptim is one of several JPEG optimizers, each with different tradeoffs:
| Tool | Algorithm | Speed | Output size | Notes |
|---|---|---|---|---|
jpegoptim | libjpeg + Huffman re-encoding | Fast | Good (5-15% lossless) | Maintained, simple CLI, great for bulk |
mozjpeg (cjpeg) | Trellis quantization, custom Huffman | Medium | Excellent (10-30% smaller than libjpeg) | Mozilla project, still actively developed |
guetzli | Perceptual psychovisual model | Very slow | Excellent | Google project, deprecated (last release 2017); slow enough to be impractical |
squoosh-cli | Wraps mozjpeg + others | Medium | Excellent (same as mozjpeg) | Modern Node CLI, supports WebP/AVIF too |
sharp-cli | libvips backend | Fast | Good | Node CLI; primarily a resizing tool, but optimizes too |
For most pipelines: jpegoptim for in-place lossless optimization (cheap and safe), mozjpeg or squoosh-cli when you want the smallest possible file at a given quality target.
mozjpeg's cjpeg takes input on stdin and produces output on stdout:
cjpeg -quality 85 -optimize -progressive image.jpg > image-opt.jpgsquoosh-cli is a single command for mixing optimizers:
npx @squoosh/cli --mozjpeg '{"quality":85}' --output-dir out/ image.jpgGuetzli existed for one reason: producing perceptually-indistinguishable JPEGs that were smaller than mozjpeg's. The project is dead and it took 1 to 2 minutes per image. Don't use it in 2026.
jpegoptim vs optipng (don't mix them)
A common confusion: jpegoptim and optipng are NOT interchangeable.
| Tool | Format | Mode | Notes |
|---|---|---|---|
jpegoptim | JPEG only | Lossless OR lossy (via --max) | Rewrites Huffman tables and optionally recompresses |
optipng | PNG only | Lossless | Tries different DEFLATE strategies and picks the smallest |
pngcrush | PNG only | Lossless | Older alternative to optipng |
pngquant | PNG only | Lossy (palette quantization) | Converts 24-bit PNG to 8-bit indexed, big savings |
Running jpegoptim on a .png file does nothing (it skips non-JPEG inputs). Running optipng on a .jpg does nothing. For a mixed-format batch, run both in parallel pipelines:
find . -type f -iname '*.jpg' -print0 | xargs -0 -P 8 jpegoptim --strip-all --max=85
find . -type f -iname '*.png' -print0 | xargs -0 -P 8 optipng -o2optipng -o2 is the standard quality/speed compromise. -o7 is the slowest and squeezes out a few more bytes; rarely worth it.
WebP and AVIF: when to skip JPEG entirely
For new content shipped to modern browsers, WebP and AVIF beat JPEG by 25 to 50 percent at equivalent perceived quality. Modern stacks ship the same source image as multiple formats and let <picture> pick the best one.
Convert JPEG to WebP with cwebp:
cwebp -q 80 image.jpg -o image.webpBulk:
find . -type f -iname '*.jpg' -print0 \
| xargs -0 -P 8 -I{} sh -c 'cwebp -q 80 "$1" -o "${1%.*}.webp"' _ {}For AVIF, use avifenc (from libavif):
avifenc --min 25 --max 35 image.jpg image.avifAVIF is the smaller of the two (often 20 to 30 percent under WebP), but encoding is 5 to 20x slower. For static sites with build-time image transformations, AVIF is worth the wait; for user-uploaded photos that need to be processed in under 100 ms, WebP is the practical pick.
Even when shipping WebP/AVIF, keep optimized JPEGs as the fallback for old clients. The <picture> element handles this:
<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="..." />
</picture>Common pitfalls
jpegoptim overwrites the source by default. No .bak, no warning. Always test on a copy first, or use --dest=/path to write to a different directory.
--max=N doesn't raise quality. If the source was saved at quality 60 and you pass --max=85, the output stays at 60 (because that's already below your max). jpegoptim only ever shrinks.
Stripping the ICC profile silently changes colors on wide-gamut images. iPhone JPEGs use Display P3. Browsers honor the embedded ICC profile; stripping it makes them assume sRGB, which desaturates everything. Use --strip-exif --strip-iptc --strip-xmp --strip-com if you want privacy without color shift.
Running jpegoptim twice doesn't compound savings. Once it's optimized, a second pass is a no-op (or a small loss if you change --max).
Bulk processing with no -P is single-threaded. A 10,000-image folder takes 30 minutes single-threaded but 4 minutes with -P 8. Always parallelize bulk work.
Progressive vs baseline. --all-progressive converts every image to progressive scan, which loads in passes (blurry first, then sharper). Progressive is slightly bigger than baseline for very small images (< 10 KB), slightly smaller for large ones. Web-default is progressive; baseline matters mainly for very simple decoders.
Permissions get reset. Without --preserve --preserve-perms, jpegoptim's output file may have different ownership or permissions than the source. On CI that copies files between stages, always use --preserve.
What to do next
If you're building image-heavy infrastructure:
- Bash For Loops: the loop and
xargs -Ppatterns that drive bulk optimization. - find Command Cheat Sheet: filter by size, extension, mtime before piping to jpegoptim.
- How to ZIP Multiple Directories Into Individual Files: package optimized image directories for client delivery or archival.
- How to Use ElasticPress with WP_Query: if your WordPress site has 100,000 image attachments, jpegoptim shrinks the bytes and ElasticPress shrinks the query time.
- PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted': when WordPress media library imports OOM during bulk uploads, jpegoptim alone won't fix it; pair this article with that one.





