DEV Community

Cover image for Building Image Compression in Rust with libvips Real Benchmarks, Real Tradeoffs
Serhii Kalyna
Serhii Kalyna

Posted on

Building Image Compression in Rust with libvips Real Benchmarks, Real Tradeoffs

I've been building Convertify a free image converter for 11 weeks now. Last week I added compression support (JPG, PNG, WebP) using Rust + libvips. Here's what I learned, with actual numbers.


The Stack

  • Backend: Rust + Axum
  • Image processing: libvips 8.15.1 via the libvips crate
  • JPEG encoder: libjpeg (bundled with libvips)
  • PNG quantization: imagequant (same algorithm as pngquant / TinyPNG)
  • PNG parser: libspng (faster than libpng)

One thing worth knowing upfront: libvips does not include mozjpeg by default. You get standard libjpeg. I'll come back to why this matters.


How the API Works

The compression endpoint is the same /api/upload used for conversion — just pass format_to=jpg (or png, webp) with a quality parameter (1–100):

curl -X POST https://convertifyapp.net/api/upload \
  -F "files=@photo.jpg" \
  -F "format_to=jpg" \
  -F "quality=80"
Enter fullscreen mode Exit fullscreen mode

On the Rust side, quality flows into libvips save options via build_output_path, which appends [Q=N] to the output filename — libvips picks it up automatically:

fn build_output_path(base: &str, format: &str, quality: Option<u32>) -> String {
    match (format, quality) {
        ("jpg" | "jpeg" | "webp", Some(q)) => format!("{}[Q={}]", base, q),
        ("png", Some(q)) => format!("{}[Q={}]", base, q),
        _ => base.to_string(),
    }
}
Enter fullscreen mode Exit fullscreen mode

Clean. No separate code path for compression vs conversion.


The Benchmarks

Test image: 1920×1080 synthetic photo, 1.5 MB original JPG (Q=95 source).

JPG → JPG (re-compression)

Quality Output size Saved
Q=90 1,172 KB 22.7%
Q=80 890 KB 41.3%
Q=70 737 KB 51.4%
Q=60 627 KB 58.6%
Q=50 542 KB 64.2%

PNG → PNG (imagequant lossy)

Test image: 1024×768, 678 KB original PNG.

Quality Output size Saved
Q=90 486 KB 29.8%
Q=80 441 KB 36.2%
Q=70 396 KB 42.7%
Q=60 377 KB 45.5%
Q=50 348 KB 49.7%

JPG → WebP (format switch as compression)

Quality Output size Saved vs original JPG
Q=90 1,120 KB 26.1%
Q=80 872 KB 42.5%
Q=70 760 KB 49.9%

The Surprising Part

WebP at Q=80 saves almost the same as JPG at Q=80 — 42.5% vs 41.3%. Not the dramatic 2× improvement you'd expect from the marketing.

Why? On already-complex photographic content (lots of high-frequency detail), the gap between WebP and JPEG narrows significantly. WebP's advantage is most visible on graphics, flat colors, and images with transparency.

The other surprise: if the source JPG is already compressed at Q=70–75 (common for web photos), re-compressing to the same format gives you almost nothing. You're fighting the JPEG DCT artifacts that are already baked in. This is where converting to WebP actually helps different codec, fresh encode.


Why Not mozjpeg?

mozjpeg typically gives 10–15% better compression than libjpeg at the same quality level. To use it with libvips you need to compile libvips from source with --with-mozjpeg. On Ubuntu:

# Not trivial requires building mozjpeg first
git clone https://github.com/mozilla/mozjpeg.git
cd mozjpeg && cmake -G"Unix Makefiles" && make install

# Then rebuild libvips pointing at mozjpeg
./configure --with-jpeg-includes=/opt/mozjpeg/include \
            --with-jpeg-libraries=/opt/mozjpeg/lib
Enter fullscreen mode Exit fullscreen mode

For a production VPS running PM2 + Caddy with system libvips, the operational complexity wasn't worth it. libjpeg at Q=80 already delivers 40%+ savings which covers 90% of use cases.


PNG: imagequant is the real hero

The selected quantisation package: imagequant line in vips --vips-config is significant. This is the same engine behind pngquant and TinyPNG's PNG compression. It works by reducing the color palette to 256 colors (lossy) while preserving perceptual quality.

The Q parameter maps to imagequant's quality range. Q=80 gives ~36% savings with barely visible quality loss on photographic PNGs. For logos and flat graphics the savings are even better since there are fewer unique colors to begin with.


The Sweet Spot

After running these benchmarks, the defaults I settled on for the UI:

  • JPG: Q=82 (hits the knee of the quality/size curve, ~40% savings)
  • PNG: Q=80 (36% savings, imagequant artifacts not visible at normal viewing)
  • WebP: Q=80 (consistent with JPG behavior)

These are close to what Squoosh and Lighthouse recommend. The difference is users can move the slider themselves the before/after size display makes the tradeoff tangible.


What's Next

The compression cluster is live at convertifyapp.net/compress/jpg. Next up: RAW format support (CR2/NEF) for photographers, and eventually mozjpeg as an opt-in for maximum compression.

If you're building something similar libvips in Rust is genuinely great. The libvips crate is well-maintained, the C library is fast (faster than ImageMagick by a wide margin), and imagequant bundled for PNG is a nice bonus.


Building in public week 11 of 52. Follow along if you're into indie dev + Rust + SEO experiments.

Top comments (0)