ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 55

  1. Blog

mozjpeg vs libvips: 385ms vs 31ms Encode

mozjpeg cjpeg 3.3.1 (mozjpeg npm 8.0.0) vs libvips jpeg via sharp 0.35.5 (mozjpeg:false) on one 1920×1080 JPEG → q80. Warm medians (7 rounds after 1 warm-up): cjpeg 385 ms / 675,128 B vs libvips jpeg 31 ms / 675,714 B (~12×). Context: sharp mozjpeg:true 308 ms / 546,546 B. Fixture /workspace/bench-mozjpeg-libvips/. cjpeg timed with spawn + PPM read.

Aditya Challa·5 October 2026·4 min read

Hands-on
On this page
  1. What I tested
  2. How wide was the encode gap?
  3. Does mozjpeg ever win this call?
  4. Should you switch JPEG encoders?
  5. Sources
  6. Related

I encoded one 1920×1080 photo at JPEG quality 80 seven times on this box: mozjpeg cjpeg 3.3.1 median 385 ms (675 KB out), libvips jpeg via sharp 0.35 median 31 ms (676 KB out) — about 12× faster at nearly the same size.

Short answer: for in-process Node pipelines, libvips jpeg was the default here. Standalone mozjpeg still fits shell scripts and imagemin-style CLI hooks. No affiliate links in this post.

What I tested

  • Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 00:26–00:28 IST.
  • Versions: mozjpeg npm 8.0.0 shipping cjpeg 3.3.1 (build 20210529); sharp 0.35.5 with mozjpeg:false for the libvips jpeg path; Node 22.20.0.
  • Fixture: /workspace/bench-mozjpeg-libvips/. Source src-1920.jpg is a 1920×1080 JPEG (~981 KB). Primary task: JPEG encode quality 80. cjpeg read a P6 PPM written from the same decode; sharp libvips path decoded the JPEG and encoded in-process. Context: sharp mozjpeg:true on the same JPEG.
  • Timing: performance.now around each encode (cjpeg via execFileSync, so spawn is included); 1 warm-up each, then 7 interleaved rounds. Outputs written to out/ for size checks.
  • Findings: mozjpeg cjpeg median 385 ms (370–476), out 675,128 B. libvips jpeg via sharp median 31 ms (29–32), out 675,714 B. sharp mozjpeg context median 308 ms / 546,546 B.
  • One surprise: at the same nominal q80, sharp mozjpeg wrote ~129 KB less than either cjpeg or libvips jpeg on this photo, at a cost closer to cjpeg than to libvips.
  • Not tested: vips CLI binary, png/webp/avif, trellis multipass flags beyond defaults, Windows or macOS, serverless cold starts.

How wide was the encode gap?

PathMedian wallMin / MaxOutput JPEG
mozjpeg cjpeg 3.3.1 q80 (from PPM)385 ms370 / 476675,128 B
libvips jpeg via sharp q8031 ms29 / 32675,714 B
sharp mozjpeg q80 (context)308 ms297 / 318546,546 B

Same source photo, same box. cjpeg times include process spawn and PPM read; libvips times are in-process. Even allowing for spawn, the gap stayed roughly an order of magnitude. For sharp vs gulp imagemin-mozjpeg, see Sharp vs imagemin. For sharp vs a JS codec on resize, see Sharp vs Jimp. For SVG optimize, see SVGO vs SVGR.

Does mozjpeg ever win this call?

Yes, when your pipeline is already a shell or imagemin plugin chain and you want mozjpeg-specific flags without taking a sharp native addon. On this Linux box, libvips jpeg returned in 31 ms median at almost the same bytes as cjpeg. If you need the smaller sharp mozjpeg output (~547 KB here), that is a different knob than plain libvips jpeg.

Treat this post as mozjpeg cjpeg vs libvips jpeg on one q80 encode, not a quality-panel shootout.

Should you switch JPEG encoders?

SituationMy pick
Node CI or server with sharp alreadylibvips jpeg (or sharp mozjpeg if size wins)
Shell / Make / imagemin CLI onlymozjpeg cjpeg can stay
Need smallest q80 file in this labsharp mozjpeg
High-volume thumbnail farmlibvips / sharp
Watching asset budgets after imagesPair with size-limit vs bundlesize and SWC vs esbuild vs Babel​
Cannot install native addonsStay on cjpeg or WASM

Bottom line: on one 1920 photo at q80, mozjpeg cjpeg returned in 385 ms median with a 675 KB file while libvips jpeg via sharp needed 31 ms and 676 KB. Who should not switch: teams whose only JPEG step is an existing cjpeg shell recipe and who cannot take sharp.

How this was made: I reused a 1920×1080 JPEG on the ShopperCove box, wrote a PPM for cjpeg, timed interleaved encodes against sharp libvips jpeg after a warm-up, recorded JSON medians and output bytes; the write-up was drafted with AI help and checked against that output.

Sources

  • https://github.com/mozilla/mozjpeg
  • https://www.npmjs.com/package/mozjpeg
  • https://www.libvips.org/
  • https://sharp.pixelplumbing.com/
  • https://www.npmjs.com/package/sharp

Related

  • https://www.shoppercove.com/blog/sharp-vs-imagemin
  • https://www.shoppercove.com/blog/sharp-vs-jimp
  • https://www.shoppercove.com/blog/svgo-vs-svgr
  • https://www.shoppercove.com/blog/size-limit-vs-bundlesize
  • https://www.shoppercove.com/blog/swc-vs-esbuild-babel
  • https://www.shoppercove.com/blog/terser-vs-esbuild-swc-minify
  • https://www.shoppercove.com/blog/vite-vs-parcel
  • https://www.shoppercove.com/blog/source-map-explorer-vs-webpack-bundle-analyzer
mozjpeglibvipssharpjpegencodingbenchmarknode.jsimage optimization

Lab evidence

What I found running this

Hands-on on ShopperCove box 6 Oct 2026 ~00:26–00:28 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). mozjpeg npm 8.0.0 / cjpeg 3.3.1 vs sharp 0.35.5 libvips jpeg (mozjpeg:false). Fixture /workspace/bench-mozjpeg-libvips/: src-1920.jpg 1920×1080 JPEG ~981 KB; cjpeg from P6 PPM; sharp from JPEG. 1 warm-up + 7 interleaved rounds. Medians cjpeg 385 ms (370–476) / 675,128 B vs libvips jpeg 31 ms (29–32) / 675,714 B (~12×). Context sharp mozjpeg 308 ms / 546,546 B. Raw: /workspace/bench-mozjpeg-libvips/res-mozjpeg-libvips.json. Not tested: vips CLI, PNG/WebP/AVIF, trellis multipass, Windows/macOS, serverless cold starts. No affiliate.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 26

    Squoosh vs Sharp: 483ms vs 291ms Encode

    @jsquash/jpeg 1.6.0 (Squoosh mozjpeg WASM) vs sharp 0.35.5 mozjpeg on one 1920×1080 JPEG → q80 encode from pre-decoded RGBA. Warm medians (7 rounds after 1 warm-up): Squoosh WASM 483 ms / 675,247 B vs sharp mozjpeg 291 ms / 546,546 B (~1.7×). Context: sharp decode+encode mozjpeg 303 ms; sharp libvips jpeg from raw 23 ms / 675,714 B. Fixture /workspace/bench-squoosh-sharp/. @squoosh/lib 0.5.3 fails on Node 22.

    5 Oct 2026

  • Plate 95

    Sharp vs imagemin: 305ms vs 399ms Encode

    sharp 0.35.5 vs imagemin 9.0.1 + imagemin-mozjpeg 10.0.0 on one 1920×1080 JPEG → JPEG q80 (no resize). Warm medians (7 rounds after 1 warm-up): sharp 305 ms / 546,546 B vs imagemin-mozjpeg 399 ms / 675,128 B (~1.3×). Context: sharp resize 800 q80 median 115 ms / 142,435 B. Fixture /workspace/bench-sharp-imagemin/.

    5 Oct 2026

  • Plate 88

    Sharp vs Jimp: 112ms vs 366ms Resize

    sharp 0.35.5 vs Jimp 1.6.1 on one 1920×1080 JPEG → width 800 JPEG q80. Warm medians (7 rounds after 1 warm-up): sharp 112 ms / 142,435 B vs Jimp 366 ms / 269,590 B (~3.3×). Fixture /workspace/bench-sharp-jimp/.

    5 Oct 2026

On this page

  1. What I tested
  2. How wide was the encode gap?
  3. Does mozjpeg ever win this call?
  4. Should you switch JPEG encoders?
  5. Sources
  6. Related
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove