DEV Community

Cover image for AVIF in sharp: what each effort level costs in time and bytes
Sahadat Husain
Sahadat Husain

Posted on

AVIF in sharp: what each effort level costs in time and bytes

Effort 3 is the best place to stop for most AVIF work in sharp. On a 1920 × 1076 photo at quality 50, effort 3 encoded in 119 ms and wrote 69.3 KB. sharp's default, effort 4, wrote 66.8 KB but took 607 ms. Effort 9 took 7,643 ms and wrote 65.8 KB, slightly more than effort 6. Below effort 3 the files get bigger and look worse.

Here is the script, what it printed, and how to choose a level for your own pipeline.

What does the effort option do in sharp's avif()?

The effort option sets how hard the encoder searches for a smaller file at the quality you asked for. The sharp output docs describe it as CPU effort between 0 (fastest) and 9 (slowest), with a default of 4. The quality default is 50.

Effort does not change the quality target. It changes how many encoding tools libaom tries before it settles. More tries usually mean fewer bytes for the same picture, and more time.

How do you measure AVIF effort levels in Node?

Decode and resize once, keep the raw pixels, then time only the encode. If you time the whole pipeline, the JPEG decode of a large photo hides the differences between low effort levels.

// effort.mjs: node effort.mjs photo.jpeg 1920
import sharp from 'sharp';

const src = process.argv[2];
const width = Number(process.argv[3] || 1920);
const { data, info } = await sharp(src).rotate().resize({ width }).raw()
  .toBuffer({ resolveWithObject: true });
const raw = { raw: { width: info.width, height: info.height, channels: info.channels } };
console.log(`input ${info.width} x ${info.height}, sharp ${sharp.versions.sharp}, libvips ${sharp.versions.vips}, aom ${sharp.versions.aom}`);

const median = (a) => a.sort((x, y) => x - y)[Math.floor(a.length / 2)];
for (let effort = 0; effort <= 9; effort++) {
  const times = [];
  let bytes = 0;
  for (let run = 0; run < 5; run++) {
    const t0 = performance.now();
    const out = await sharp(data, raw).avif({ quality: 50, effort }).toBuffer();
    times.push(performance.now() - t0);
    bytes = out.length;
  }
  console.log(`effort ${effort}  ${(bytes / 1000).toFixed(1).padStart(6)} KB  ${Math.round(median(times)).toString().padStart(5)} ms`);
}
Enter fullscreen mode Exit fullscreen mode

I ran it on a 4964 × 2782 camera photo (a lake at sunset with a boat), resized to 1920 pixels wide:

input 1920 x 1076, sharp 0.35.4, libvips 8.18.6, aom 3.15.0
effort 0    82.7 KB     22 ms
effort 1    78.4 KB     40 ms
effort 2    79.3 KB     79 ms
effort 3    69.3 KB    119 ms
effort 4    66.8 KB    607 ms
effort 5    66.9 KB    958 ms
effort 6    65.1 KB   1769 ms
effort 7    65.5 KB   2795 ms
effort 8    65.5 KB   2905 ms
effort 9    65.8 KB   7643 ms
Enter fullscreen mode Exit fullscreen mode

Bar charts of AVIF output size and encode time for sharp effort 0 to 9 on a 1920 x 1076 photo

Where does the time go between effort 3 and effort 9?

The curve has two shapes. Bytes fall fast from effort 0 to effort 3, then flatten. Time grows slowly to effort 3, then jumps.

Step Bytes saved Extra time
effort 0 to 3 13.4 KB (16%) 97 ms
effort 3 to 4 2.5 KB (3.6%) 488 ms
effort 4 to 6 1.7 KB (2.5%) 1,162 ms
effort 6 to 9 0.7 KB more, not less 5,874 ms

Effort 3 to effort 4 is the cliff. One level costs 5 times the encode time for 3.6% of the bytes. Past effort 6, the file did not shrink at all on this photo: effort 7, 8 and 9 all came out a little larger than effort 6.

Does a higher effort level change image quality?

Only a little, and the low levels lose on both counts. I decoded each output and computed PSNR against the resized pixels:

q50 effort 0  82.7 KB  PSNR 38.06 dB
q50 effort 3  69.3 KB  PSNR 39.12 dB
q50 effort 4  66.8 KB  PSNR 39.37 dB
q50 effort 6  65.1 KB  PSNR 39.47 dB
q50 effort 9  65.8 KB  PSNR 39.58 dB
Enter fullscreen mode Exit fullscreen mode

Effort 0 is the worst trade on the list. The file is 19% larger than effort 3 and scores about 1 dB lower. The speed buys you nothing you would keep.

From effort 3 upward, PSNR moves by less than half a decibel. So the higher levels mostly buy bytes, not a visibly better picture.

Note: Do not compare effort levels by PSNR at a fixed quality alone. A faster level sits on a slightly different curve, so most of its loss shows up in bytes. If you need the same file size from effort 3 as from effort 4, lower the quality a step instead: at effort 3, quality 48 wrote 60.6 KB at 38.58 dB.

Does the gap grow on a full-size photo?

Yes, in time. I ran levels 0, 3, 4 and 6 on the same photo at its full 4964 × 2782:

input 4964 x 2782, sharp 0.35.4, libvips 8.18.6, aom 3.15.0
effort 0   326.1 KB    135 ms
effort 3   288.0 KB    644 ms
effort 4   271.3 KB   2821 ms
effort 6   264.7 KB   8367 ms
Enter fullscreen mode Exit fullscreen mode

At 13.8 megapixels, effort 4 took 4.4 times as long as effort 3 for 5.8% fewer bytes. Effort 6 took 8.4 seconds. That is fine for a one-off build step. It is not fine inside a request handler.

Which effort level should you use?

Pick by where the encode runs and how often:

  • Inside a request, or a queue with many uploads: effort 3. You keep most of the byte savings and stay under a second for large photos.
  • A static build that runs once per image: effort 4 to 6. The extra seconds happen once and every visitor gets the smaller file.
  • Never: effort 0 to 2 for photos, and effort 7 to 9 unless you have measured a gain on your own images.

Our engine settled on effort 3 for the same reason. The 1920-pixel output above, 69.3 KB, is the same 69.3 KB our engine produces for this photo at 1920 wide, and the AVIF vs WebP vs JPEG comparison shows how that file sits against the other formats.

How I measured

  • Photo: apps/web/public/images/index/image-compressor-sample-camera-photo.jpeg, 4964 × 2782, our own camera photo.
  • Encode: sharp 0.35.4, libvips 8.18.6, libaom 3.15.0, Node 24.22.0, avif({ quality: 50, effort }), sharp's default 4:4:4 chroma and 8-bit depth.
  • Time: encode only, from raw pixels, median of 5 runs per level, performance.now().
  • PSNR: computed in Node over RGB against the resized raw pixels, one run per setting.
  • Machine: Apple M1 Pro (8 cores), macOS, 2026-10-17. sharp ran with its default thread pool.

What I did not test

One photo is not every photo; flat graphics and screenshots have their own curve. I did not test 10-bit output, 4:2:0 chroma, or Linux servers, where libaom's timing can differ.


I build Blinkimg, an image compressor and converter with a developer API. I wrote this article with help from an AI assistant (Claude), which ran the code on my machine; every number in it comes from those runs.

Top comments (0)