Skip to content

dqev/lessbytes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lessbytes Logo

Lessbytes

Compress images without guessing quality. Get the smallest file that still looks identical.

npm version bundle size zero browser deps types

Live demo  ·  API  ·  CLI


Most image compressors ask you to pick a quality number and hope for the best. The right number is different for every image — a screenshot compresses differently than a photo. Hardcoding quality: 80 works until it doesn't.

Lessbytes treats quality as a search, not a setting. You set a perceptual target. It binary-searches the quality range, encodes the image, decodes it, measures SSIM against the original, and finds the smallest file that still looks the same — in seven iterations.


How it compares

Fixed-quality compression Lessbytes
Quality decision You set it upfront Per-image binary search
Accuracy None — you eyeball it SSIM score vs. original
Format Whatever you pick AVIF, WebP, JPEG competed; smallest wins
Photos May look degraded Holds quality where it counts
Flat graphics Wastes bytes Strips what the eye can't see
Typical result Smaller, sometimes ugly ~84% smaller, looks the same

Real example from the test suite: 235 KB JPEG → 37 KB WebP at SSIM 0.992.


Supported formats

JPEG · PNG · WebP · AVIF — with runtime detection so AVIF is only used when the encoder is available.


How it works

Five steps. Step four is where the magic is.

lessbytes compression pipeline

The search. Each iteration: encode at a candidate quality → decode back to pixels → compute SSIM against the original. If it passes the threshold, try lower quality (smaller file). If not, back off. Seven iterations converge within 1% quality resolution.

Downscaling. Instead of one bilinear pass (which blurs fine detail), Lessbytes steps down in stages. Slower, but the output looks better at small dimensions.

Format competition. In auto mode, all supported formats are encoded in parallel. The smallest one that meets the SSIM target is used. You don't decide; the numbers do.

SSIM score reference:

Score What it means
1.000 Identical pixels
≥ 0.998 Below the threshold of human vision
~0.970 Visible only in a direct side-by-side comparison
< 0.950 Clearly degraded

The default target is 0.992 — chosen by testing against many different images. It's the lowest value where no visible degradation appears without a diff view.


Install

npm install lessbytes

Or in the browser — no build step needed:

<script src="https://unpkg.com/lessbytes"></script>
<script>
  lessbytes.compress(file).then(r => console.log(`${(r.ratio * 100).toFixed(1)}% smaller`));
</script>

API

// ESM / bundlers (Vite, webpack, Rollup, esbuild)
import { compress, compressBatch, computeSSIM, isFormatSupported } from 'lessbytes';

// CommonJS
const { compress } = require('lessbytes');

// Browser, no build step — exposes a global `lessbytes`
// <script src="https://unpkg.com/lessbytes"></script>

compress(input, options?) → Promise<CompressResult>

input: File | Blob | HTMLImageElement | URL string.

const result = await compress(file, {
  format: 'webp',         // 'webp' | 'auto' | 'jpeg' | 'png' | 'avif'
  targetSSIM: 0.992,      // raise for more quality; lower to push smaller
  maxWidth: 1920,         // aspect ratio preserved, never upscales
  maxHeight: null,
  targetSize: 100 * 1024, // hard ceiling in bytes — useful for upload limits
  background: '#ffffff',  // fill used when flattening alpha into a lossy format
  keepSmallest: true      // return original if compression makes it larger
});

result.blob     // Blob — the output
result.format   // 'webp'
result.ssim     // 0.9924
result.ratio    // 0.84   (84% smaller)
result.quality  // 0.93   (quality the search settled on)
result.toFile('upload.webp') // wraps blob as a File

A typical upload handler:

input.addEventListener('change', async () => {
  const { blob, toFile } = await compress(input.files[0], { maxWidth: 1920 });
  preview.src = URL.createObjectURL(blob);
  formData.append('photo', toFile('photo.webp'));
});

compressBatch(inputs, options?) → Promise<CompressResult[]>

Process multiple images with a concurrency limit and a progress callback.

const results = await compressBatch(files, {
  concurrency: 3,
  maxWidth: 1600,
  onProgress: (done, total, last) => {
    console.log(`${done}/${total} — last: ${(last.ratio * 100).toFixed(0)}% smaller`);
  }
});

computeSSIM(a: ImageData, b: ImageData) → number

The same SSIM function the search loop uses, exposed directly. Both buffers must be the same size. Useful for validating compression quality in your own pipeline.

isFormatSupported(mime: string) → boolean

Checks whether the browser can encode a given mime type. Lessbytes uses this internally before trying AVIF. You can use it to show or hide format options in your UI.


CLI

The CLI uses sharp (libvips) for fast Node-side encoding. It's an optional dependency — the browser bundle doesn't know about it.

lessbytes CLI

Installation

Install globally to get the lessbytes command. Running via npx is blocked.

npm install -g lessbytes

lessbytes <files...|dir> [options]

If sharp fails to install, the CLI tells you exactly how to add it:

npm install -g sharp     # if lessbytes is installed globally
npm install sharp        # for a local project

Interactive mode

Run lessbytes with no arguments to launch the step-by-step interface:

lessbytes
lessbytes interactive interface
  Image file or folder › photo.jpg

  Output format
    1  Auto — compete AVIF / WebP / JPEG, keep the smallest  (default)
    2  WebP
    3  AVIF
    4  JPEG
    5  PNG
  › 1

  Compression mode
    1  Smart — visually lossless (SSIM-guided search)  (default)
    2  Target file size
    3  Fixed quality
  › 1

  Max width in px (blank = no limit) › 1920
  Output path (blank = beside source) › build/img
Interactive mode Equivalent flag What it does
Smart (default) SSIM-guided search for the smallest visually-lossless file
Target file size --max-size Searches quality, then downscales to hit the byte budget
Fixed quality --quality Encodes once at the quality you set — no search

Use -i / --interactive to force the interface in a non-interactive shell.

Direct usage

lessbytes photo.jpg                        # → photo.min.webp, beside the source
lessbytes *.png -o out/                    # batch a glob into a folder
lessbytes hero.png --format webp --quality 80
lessbytes banner.jpg --max-size 100kb      # hit a hard size budget
lessbytes huge.jpg --max-width 1920        # cap dimensions, keep aspect ratio
lessbytes ./assets -r -o build/img         # recurse a directory tree
lessbytes shot.png --format avif --ssim 0.995
lessbytes icon.png --keep-larger           # always write, even if bigger

All flags

Flag Description Default
-o, --output <path> Output file or directory *.min.* beside source
-f, --format <fmt> auto · jpeg · webp · png · avif webp
-q, --quality <1-100> Force a fixed quality — skips the SSIM search
--max-size <size> Hard size ceiling, e.g. 100kb, 1.5mb, 512b
--max-width <px> Cap output width, aspect ratio preserved
--max-height <px> Cap output height, aspect ratio preserved
--ssim <0-1> Perceptual target for the quality search 0.992
-r, --recursive Recurse into subdirectories off
--suffix <str> Suffix appended for in-place output .min
--keep-larger Write output even if it ends up bigger than the source off
-s, --silent Print errors only off
-i, --interactive Force the interactive interface off
--logo Print the logo banner and exit
-h, --help Show help
-v, --version Print the version

Format selection

  • webp (default) — encodes to WebP.
  • auto — opaque images compete WebP, AVIF, and JPEG; transparent images compete WebP, AVIF, and PNG. The smallest result wins. AVIF is skipped if your sharp build can't encode it.
  • Explicit format — only that format is tried. JPEG flattens transparency onto the background color (white by default).

How --max-size works

It's a true ceiling. First Lessbytes searches quality. If even the lowest quality is still too large, it progressively scales down dimensions (80% steps) and searches again. If a target is impossible (e.g. --max-size 1b), it writes the smallest it can and marks the row with ⚠ over target.

Reading the output

lessbytes v1.2.0  compressing 1 image
  ✓ photo.jpg   235.8 KB → 36.9 KB   -84%   webp q93 ssim 0.992

Done. 235.8 KB → 36.9 KB  (84.4% smaller)

Each row shows: file name, original → output size, percent saved, format used, quality the search settled on, and the measured SSIM. Downscaled results show e.g. 60% scale. If the original was smaller, it shows kept original.

Exit codes

Code Meaning
0 All images processed (or nothing to do)
1 Bad arguments, no images found, sharp missing, or every image failed

A run where some images succeed and some fail exits 0. Failures are shown per row.


Browser compatibility

Works in any browser that supports Canvas and toBlob — which covers everything released in the last several years. AVIF encoding is checked at runtime via isFormatSupported and falls back to WebP. JPEG is the universal fallback. WebP encoding works in ~97% of active browser installs.

Node: 16+ for the CLI. The browser library has no Node dependency.

Bundlers: ships ESM, CommonJS, and UMD. Tree-shakeable. Works with Vite, webpack, Rollup, esbuild, and plain <script> without any config.


Design notes

Why binary search and not a perceptual codec? Libraries like squoosh use encoder-side perceptual tuning (e.g. butteraugli). That's more precise but needs WebAssembly and a larger bundle. Lessbytes's SSIM search is a close approximation that runs on Canvas only — no Wasm, no build dependencies.

Why 0.992 as the default? It was tested against a wide range of photos and illustrations. At 0.992, no visible degradation appeared in A/B comparisons. Going lower occasionally shows subtle banding on portraits. Going higher uses more bytes for no visible gain.

Why return the original when compression would make it larger? Because the right behavior for an upload handler is to fail gracefully. A 9 KB PNG doesn't benefit from WebP encoding — Lessbytes returns it unchanged instead of making you handle that case yourself.


License

MIT © Dev


Built by Dev

Compression should be measured, not guessed.

About

Image compression that converges on visually lossless. SSIM-guided binary search finds the smallest file the human eye can't distinguish from the original. Zero dependencies in the browser; sharp-powered CLI for Node.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors