TechEarl

How to Optimize JPEG Images Using jpegoptim

Use jpegoptim to losslessly or lossy-compress JPEGs from the command line, in bulk, and inside CI pipelines. Includes the install path on macOS/Linux/Windows, mozjpeg / squoosh-cli / sharp comparisons, and the parallel xargs pattern for tens of thousands of images.

Ishan KarunaratneIshan Karunaratne⏱️ 12 min readUpdated
jpegoptim CLI usage: lossless and lossy modes, --max quality, --strip-all metadata removal, bulk find + xargs -P parallel pipelines, comparisons with mozjpeg, squoosh-cli, and sharp-cli.

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

macOS (Homebrew):

bash
brew install jpegoptim

Debian / Ubuntu:

bash
sudo apt update
sudo apt install jpegoptim

Fedora / RHEL:

bash
sudo dnf install jpegoptim

Alpine (used in many Docker images):

bash
apk add jpegoptim

Windows: 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:

bash
jpegoptim --version

Expect 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).

bash
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):

bash
jpegoptim --max=85 image.jpg

If 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:

bash
jpegoptim --size=150k image.jpg
jpegoptim --size=80% image.jpg

The 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.

bash
jpegoptim --strip-all image.jpg

--strip-all removes every optional marker. For more surgical control:

FlagRemoves
--strip-allevery optional marker
--strip-exifEXIF only (GPS, camera info, timestamps)
--strip-iptcIPTC (caption, byline, copyright)
--strip-iccICC color profiles
--strip-xmpXMP metadata
--strip-comcomment markers
--strip-jfifJFIF (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":

bash
find . -type f \( -iname '*.jpg' -o -iname '*.jpeg' \) -print0 \
  | xargs -0 -P 8 -n 50 jpegoptim --strip-all --max=85

Breaking it down:

  • find . -type f recurses into all subdirectories looking for files only.
  • -iname '*.jpg' -o -iname '*.jpeg' matches both extensions, case-insensitive.
  • -print0 separates results by NUL bytes, so filenames with spaces or newlines work safely.
  • xargs -0 reads NUL-separated input.
  • -P 8 runs up to 8 jpegoptim processes in parallel.
  • -n 50 passes 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):

bash
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:

bash
mkdir -p /var/optimized
jpegoptim --strip-all --max=85 --dest=/var/optimized image.jpg

The --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:

ToolAlgorithmSpeedOutput sizeNotes
jpegoptimlibjpeg + Huffman re-encodingFastGood (5-15% lossless)Maintained, simple CLI, great for bulk
mozjpeg (cjpeg)Trellis quantization, custom HuffmanMediumExcellent (10-30% smaller than libjpeg)Mozilla project, still actively developed
guetzliPerceptual psychovisual modelVery slowExcellentGoogle project, deprecated (last release 2017); slow enough to be impractical
squoosh-cliWraps mozjpeg + othersMediumExcellent (same as mozjpeg)Modern Node CLI, supports WebP/AVIF too
sharp-clilibvips backendFastGoodNode 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:

bash
cjpeg -quality 85 -optimize -progressive image.jpg > image-opt.jpg

squoosh-cli is a single command for mixing optimizers:

bash
npx @squoosh/cli --mozjpeg '{"quality":85}' --output-dir out/ image.jpg

Guetzli 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.

ToolFormatModeNotes
jpegoptimJPEG onlyLossless OR lossy (via --max)Rewrites Huffman tables and optionally recompresses
optipngPNG onlyLosslessTries different DEFLATE strategies and picks the smallest
pngcrushPNG onlyLosslessOlder alternative to optipng
pngquantPNG onlyLossy (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:

bash
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 -o2

optipng -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:

bash
cwebp -q 80 image.jpg -o image.webp

Bulk:

bash
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):

bash
avifenc --min 25 --max 35 image.jpg image.avif

AVIF 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:

html
<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:

FAQ

TagsLinuxImage OptimizationJPEGCommand LinejpegoptimmozjpegWebPAVIFPerformance
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Macro photograph of a stack of paper documents on a dark slate desk with a single sheet pulled out and crumpled to the side, warm amber side light

How to Delete Duplicate Rows in MySQL

Delete duplicate rows in MySQL while keeping one per group, using DELETE JOIN, ROW_NUMBER with CTE, or the safe temp-table swap. With dry-run, transactions, and rollback.

The grep -o | sort | uniq -c | sort -rn pipeline counts unique matches and ranks them. Why sort comes before uniq, worked log-analysis examples, sort -u, uniq -d, and the awk one-pass alternative.

How to Count Unique Matches with grep, sort, and uniq

The grep -o 'pattern' file | sort | uniq -c | sort -rn pipeline is the classic log-analysis one-liner. Why sort must come before uniq, how each stage works, worked examples for top IPs and status codes, the awk one-pass alternative for huge files, and the BSD vs GNU notes.