Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

optimize — make images, GIFs and video small enough for the web

npm node license


Start here

# Ask me questions
npx @solithix/optimize

# Just do it
npx @solithix/optimize ./assets --profile web --out ./dist

# Show me what would happen, change nothing
npx @solithix/optimize ./assets --dry-run

No config. No setup. Point it at a folder.


What it saves

Measured savings: PNG to AVIF −83%, PNG to WebP −75%, JPEG to WebP −63%, GIF to animated WebP −41%, MP4 −29%

Every number on this page came out of the tool. None of them are estimates.

Input Becomes Handled by
JPEG, PNG, WebP, TIFF, AVIF, SVG WebP, AVIF, JPEG, PNG sharp
GIF animated WebP, optimized GIF, MP4, WebM sharp
MP4, MOV, MKV, AVI, WebM, and friends H.264 MP4, VP9 WebM, plus a poster frame ffmpeg

GIFs never touch ffmpeg. sharp reads and writes animated GIF and WebP on its own, so the common path needs no external binary at all. Without ffmpeg installed you lose video and nothing else.


Profiles

Pick one instead of answering twenty questions.

Profile What it does
web The default. WebP at quality 80, capped at 1920 px.
balanced Gentler. Higher quality, no resize.
maximum Smallest files. Uses AVIF, so it is slow.
lossless No quality loss. Files may grow, and the tool refuses when they do.
npx @solithix/optimize ./img --profile maximum

It is fast because it stopped waiting

40 images, 77 MB: 19.03s one at a time, 6.01s with 4 in parallel, 3.87s with 12 in parallel

The trick is not the pool size — it is when you set it. libuv's thread pool caps how many images sharp can process at once, it defaults to 4, and it is built during runtime start-up. Setting UV_THREADPOOL_SIZE from inside the app does nothing at all. So the entry point re-execs once with the pool sized to your core count, which costs about 85 ms and pays for itself on the first file.


It will not eat your files

Atomic write: the original is read, a scratch file is written beside it, then renamed into place

Most of this exists because the previous version got it wrong.

  • Originals are safe by default. Results go to an optimized/ folder.
  • Nothing is written in place. Every file lands beside its target and is renamed in. A crash, a cancel, or a failed encode leaves the original alone.
  • It refuses to make files bigger. Larger output is thrown away and reported. --force overrides.
  • Collisions are numbered, not overwritten. Photo 1.jpg and photo-1.jpg both sanitize to the same name; you get both files, not one.
  • Tiny files are skipped. Below 10 KB the encoding overhead beats the saving.

How it is built

Architecture: scan and plan feed parallel encoders, then an atomic commit; a RunEvent stream feeds the Ink wizard, the text reporter and --json

The engine emits a typed event stream. The wizard, the terminal reporter, and --json are all just subscribers to it — which is the only reason a headless mode is possible. Put the processing loop inside a React component and nothing can run without a UI.


Flags

--out, -o <dir>        Write results here
--suffix <text>        Same folder, add this to the name
--replace              Overwrite the originals (needs --force)
--dry-run              Measure the saving, write nothing
--json                 One JSON document, nothing else

--profile, -p <name>   web | balanced | maximum | lossless
--format, -f <fmt>     webp | avif | jpeg | png | keep
--quality, -q <1-100>
--width, -w <px>       Largest image width
--gif <fmt>            webp | gif | mp4 | webm
--concurrency, -c <n>  Files at once (default: core count)

--force                Write even when the result is bigger
--yes, -y              Skip confirmations

Exit code is 0 on success, 1 when a file failed, 2 for a bad command — so CI can actually tell.

In a pipeline

optimize ./public/img --profile web --out ./dist/img --json > report.json
{
  "ok": true,
  "summary": { "total": 12, "succeeded": 11, "skipped": 1, "before": 2011882, "after": 812344 },
  "skipped": [{ "name": "logo.png", "reason": "larger", "wouldBe": 431023 }]
}

Config file

Drop an optimize.config.json beside your package.json, or add an optimize key to it. Flags beat the config file; the config file beats the profile.

{
  "profile": "web",
  "image": { "format": "webp", "quality": 82, "maxWidth": 1600 },
  "animation": { "output": "webp" },
  "output": { "mode": "folder", "dir": "dist/assets" }
}

Requirements

  • Node.js ≥ 20.9
  • FFmpeg — only for video. A binary ships as a fallback, and the tool runs happily without one.

Notes

The illustrations above are generated, not drawn:

node docs/assets/generate.mjs docs/assets

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages