squeezit
v1.17.0
Published
Lossless image optimizer CLI, JS/TS API, and bundler plugins for PNG, JPEG, GIF, WebP, SVG, AVIF, HEIC, JXL, ICO, BMP, and more.
Maintainers
Readme
squeezit is a CLI for aggressively compressing images without casually degrading them. It is designed for codebases, asset folders, and content repositories where you want smaller files, predictable behavior, and a command you can trust in day-to-day workflows.
It supports direct file paths, shell-style patterns like *.png, glob expressions like images/**/*.webp, and a no-argument mode that scans supported image files in the current directory. Recursive scanning is available when you ask for it.
Why Squeezit
- Lossless-first workflow across common web and design formats
- APNG, JXL, and ICO support alongside common web asset formats
- Friendly CLI output with clear summaries, skips, and failures
- Safe threshold-based replacement so tiny wins do not churn your files
- Pattern matching that works with both regular shell parameters and glob expressions
- Works well as a local cleanup tool before commits or releases
- Published for Node users, while Bun remains the development and build toolchain
Installation
npm
npm install -g squeezitbun
bun add -g squeezitAfter installation, both commands are available:
squeezit --help
sqz --helpQuick Start
Compress supported images in the current directory:
squeezitPreview changes without modifying files:
squeezit -dStrip metadata only, without recompressing:
squeezit --exifTarget only top-level PNGs:
squeezit "*.png"Target nested files with glob expressions:
squeezit -r "images/**/*.{png,jpg,webp}"Optimize an icon container in dry-run mode:
squeezit favicon.ico -dRun the shorter alias:
sqz -r assets/**/*.jpg -dCheck for a newer published version:
squeezit --check-updateSelf-update to the latest release:
squeezit -UIntegrations
squeezit is now structured to expose first-party integrations from the same package.
Available today:
- Root JS/TS API via
import { optimizeFile, optimizeFiles, stripMetadata } from "squeezit" - Gulp plugin via
import { squeezitGulp } from "squeezit/gulp" - Grunt plugin via
const { registerSqueezitTask } = require("squeezit/grunt") - Vite plugin via
import { squeezitVite } from "squeezit/vite" - Webpack plugin via
import { squeezitWebpack } from "squeezit/webpack" - Rollup plugin via
import { squeezitRollup } from "squeezit/rollup" - Parcel optimizer plugin with implementation exported at
squeezit/parcel - Astro wrapper via
import { squeezitAstro } from "squeezit/astro" - Next.js wrapper via
import { withSqueezit } from "squeezit/next" - esbuild plugin via
import { squeezitEsbuild } from "squeezit/esbuild" - Babel plugin via
import { squeezitBabel } from "squeezit/babel"
The supported programmatic integration surfaces are the root JS/TS API, the Gulp plugin, the Grunt plugin, the Vite plugin, the Webpack plugin, the Rollup plugin, the Parcel optimizer plugin, the Astro wrapper, the Next.js wrapper, the esbuild plugin, and the Babel plugin.
Gulp
const { src, dest } = require("gulp");
const { squeezitGulp } = require("squeezit/gulp");
exports.images = function images() {
return src("assets/**/*").pipe(squeezitGulp()).pipe(dest("dist/assets"));
};The Gulp plugin runs as a Vinyl transform, uses the default compression strategy, and always enables metadata stripping. It supports buffered Vinyl files, stream-backed Vinyl files by buffering them internally before optimization, and path-backed null Vinyl files when file.path is available. Null Vinyl files without a usable path pass through unchanged. It does not expose or use max mode.
Grunt
const { registerSqueezitTask } = require("squeezit/grunt");
module.exports = function (grunt) {
registerSqueezitTask(grunt);
grunt.initConfig({
squeezit: {
images: {
files: [
{
src: ["assets/**/*.{png,jpg,webp,svg}"],
dest: "dist/assets",
},
],
},
},
});
};The Grunt plugin registers a real multi-task, uses the default compression strategy, and always enables metadata stripping. v1 respects Grunt file mappings, so it can optimize files in place or write optimized output to mapped destination paths. It does not expose or use max mode.
Vite
import { defineConfig } from "vite";
import { squeezitVite } from "squeezit/vite";
export default defineConfig({
plugins: [squeezitVite()],
});The Vite plugin runs only for production builds, optimizes emitted assets from the output directory, uses the default compression strategy, and always enables metadata stripping. It does not expose or use max mode.
Webpack
const { squeezitWebpack } = require("squeezit/webpack");
module.exports = {
plugins: [squeezitWebpack()],
};The Webpack plugin runs after assets are written to the configured output directory, optimizes emitted image files from that directory, uses the default compression strategy, and always enables metadata stripping. It does not expose or use max mode.
Rollup
import { defineConfig } from "rollup";
import { squeezitRollup } from "squeezit/rollup";
export default defineConfig({
input: "src/index.js",
output: {
dir: "dist",
format: "esm",
},
plugins: [squeezitRollup()],
});The Rollup plugin optimizes emitted image assets with the default compression strategy and always enables metadata stripping. It prefers in-memory asset optimization during bundle generation, including hash-safe filename/reference updates when asset bytes change, and uses a post-write output pass as a fallback for emitted files not already handled in memory. It does not expose or use max mode.
Parcel
squeezit/parcel exports the Parcel optimizer implementation, but Parcel itself only accepts optimizer specifiers that match its parcel-optimizer-* naming convention. When consuming it from the main squeezit package, point .parcelrc at the built plugin file inside node_modules:
.parcelrc
{
"extends": "@parcel/config-default",
"transformers": {
"url:*": ["@parcel/transformer-raw"]
},
"optimizers": {
"*.{png,gif,webp,svg,heif,heic,avif,bmp,ico,cur,jxl}": [
"...",
"./node_modules/squeezit/dist/parcel.cjs"
]
}
}package.json
{
"squeezit": {
"parcel": {
"enabled": true,
"checkDependencies": true,
"productionOnly": true
}
}
}The Parcel integration is a real Parcel optimizer plugin. It runs in Parcel's asset pipeline, uses the default compression strategy, always enables metadata stripping, and is production-only by default. Its small config surface is read from package.json under squeezit.parcel, and it does not expose or use max mode.
Astro
import { defineConfig } from "astro/config";
import { squeezitAstro } from "squeezit/astro";
export default defineConfig({
output: "static",
integrations: [squeezitAstro()],
});The Astro wrapper is a thin integration over squeezit/vite. It only matters for astro build, targets static Astro output in v1, uses the default compression strategy, and always enables metadata stripping. SSR and hybrid Astro output are not supported by this wrapper yet, and it does not expose or use max mode.
Next.js
const { withSqueezit } = require("squeezit/next");
module.exports = withSqueezit({
webpack(config) {
return config;
},
});The Next.js wrapper augments webpack-based Next builds by injecting the squeezit Webpack plugin through next.config.js/next.config.ts. It uses the default compression strategy, always enables metadata stripping, and does not expose or use max mode.
Turbopack support is not included in this wrapper yet. It is planned and coming soon, but this integration currently targets Next’s webpack build pipeline only.
esbuild
import { build } from "esbuild";
import { squeezitEsbuild } from "squeezit/esbuild";
await build({
entryPoints: ["src/index.ts"],
outdir: "dist",
bundle: true,
plugins: [squeezitEsbuild()],
});The esbuild plugin runs after a successful disk-backed build, optimizes emitted image files from the written output directory, uses the default compression strategy, and always enables metadata stripping. It does not expose or use max mode.
Babel
import { squeezitBabel } from "squeezit/babel";
export default {
plugins: [[squeezitBabel, { productionOnly: true }]],
};The Babel plugin is narrower than the emitted-asset integrations. It runs at compile time, is production-only by default, rewrites static local image imports and static JSX string-literal asset references, and writes optimized generated copies under .squeezit/babel-assets. It does not mutate source assets and does not expose or use max mode.
The fixture-value helper and JS/TS API report filePath and outputPath relative to the effective cwd, not as absolute machine-specific paths.
Documentation
Usage
squeezit [patterns...] [options]Pattern Resolution
- Pass explicit file paths like
hero.png - Pass shell-style parameters like
*.png - Pass glob expressions like
images/**/*.webp - Pass directories like
assets - If no file parameter is provided,
squeezitscans supported image files in the current directory - Scanning is non-recursive by default; use
-r, --recursiveto traverse subdirectories - Discovery includes APNG (
.apng), JPEG XL (.jxl), ICO (.ico), and CUR (.cur) files
Options
| Option | Description | Default |
| ------------------------- | ------------------------------------------------------------------------------------- | ------------------------------ |
| -r, --recursive | Recurse into directories when scanning inputs | false |
| -m, --max | Use the heaviest lossless compression passes, strip metadata, and force threshold 0 | false |
| -s, --strip-meta | Remove EXIF, IPTC, and XMP metadata during compression | false |
| --exif | Only strip EXIF/IPTC/XMP metadata without recompressing | false |
| -d, --dry-run | Show what would change without writing files | false |
| -k, --keep-time | Preserve original access and modification timestamps | false |
| -c, --concurrency <n> | Set worker concurrency manually | CPU count, or 2 with --max |
| -I, --install-deps | Attempt to install missing system tools | false |
| -U, --update | Update squeezit to the latest published version | false |
| --check-update | Check whether a newer published version exists | false |
| --pm <manager> | Override the package manager used for self-update | auto-detected when possible |
| -v, --verbose | Print additional diagnostic details | false |
| -t, --threshold <bytes> | Minimum savings required before replacing a file | 100 |
| -i, --in-place | Create temporary work artifacts next to the source files | false |
| -V, --version | Print the current version | n/a |
| -h, --help | Show CLI help | n/a |
Examples
Preview everything under the current directory:
squeezit -dCompress a single file:
squeezit ./images/cover.pngCompress every JPEG under assets, but only if the win is at least 1 KB:
squeezit -r "assets/**/*.jpg" -t 1024Use the heaviest compression strategy:
squeezit -r "images/**/*" -mStrip metadata only:
squeezit --exif "photos/**/*.{jpg,tiff,heic}"Preserve timestamps while stripping metadata:
squeezit -r "photos/**/*.{jpg,tiff,heic}" -s -kDry-run a JPEG XL file:
squeezit artwork.jxl -dModernize an ICO while preserving its icon sizes:
squeezit app.icoModernize a cursor container while preserving entry sizes and hotspots:
squeezit pointer.curUpdate the global installation explicitly with npm:
squeezit -U --pm npmSupported Inputs
Squeezit currently matches these file extensions during discovery:
jpg,jpegpng,apnggifwebpsvgtif,tiffheic,heifavifbmpjxlico,curcr2,nef,arw,raf,orf,rw2
Internally, compression behavior is determined with MIME detection where applicable, not only by extension.
Supported Formats
Squeezit currently supports these image format families:
JPEG(.jpg,.jpeg): fast lossless optimization by default, heavier passes in--maxPNG(.png): fastoxipngoptimization by default, heavier candidate comparison in--maxAPNG(.apng, animated PNG payloads): optimized losslessly withoxipngGIF(.gif): fast lossless optimization by default, strongestgifsiclepass in--maxWebP(.webp): lossless re-encode, with heavier encoder settings in--max, including animated WebP handlingSVG(.svg): single-pass optimization by default, multipass in--maxTIFF(.tif,.tiff): lossless ZIP recompression, with a heavier ZIP preset in--maxHEIF / HEIC(.heif,.heic): lossless re-encode, with a slower encoder preset in--maxAVIF(.avif): lossless re-encode, with a slower encoder speed in--maxBMP(.bmp): lossless RLE recompression for source 4-bit and 8-bit BMPs only; higher-bit BMPs are skippedJPEG XL(.jxl): lossless re-encode, with a faster default pass and multi-effort candidate comparison in--maxICO(.ico): modernized by extracting embedded icon images, optimizing them, and rebuilding the icon container while preserving the original entry dimensions; if the rebuilt icon changes the dimension set, it is skippedCUR(.cur): modernized by extracting embedded cursor images, optimizing them, and rebuilding the cursor container while preserving the original entry dimensions and hotspot coordinates; if the rebuilt cursor changes either, it is skippedRAW camera files(.cr2,.nef,.arw,.raf,.orf,.rw2): metadata stripping in--exifmode, optional RAW-to-DNG conversion in--maxmode using the smallest lossless DNG settings
Notes:
- If a lossless result is larger, the file is skipped and never replaced
--exifis metadata-only mode and does not run recompression pipelines--maxalways strips metadata in addition to raising encoder effort across the supported recompression pipelines--maxforces the replacement threshold to0, so any positive lossless reduction is accepted- ICO support is focused on modernizing containers while preserving icon sizes, not preserving original legacy BMP-style encoding byte-for-byte
- CUR support is focused on modernizing containers while preserving entry sizes and cursor hotspots, not preserving original legacy BMP-style encoding byte-for-byte
- BMP metadata-only writing is not supported; BMP optimization only rewrites eligible indexed BMP image data
- ICO and CUR metadata-only writing are not supported
- RAW files are special-case inputs and only convert to
.dngin--maxmode - RAW
--maxconversion now targets the smallest lossless DNG by disabling embedded RAW, preview, and thumbnail payloads;.rw2inputs also try the available lossless JPEG predictor variants and keep the smallest result
System Dependencies
Squeezit orchestrates native image tools based on the inputs you actually process. It may require binaries such as:
filejpegtran,jpegrescan,jpegoptimpngcrush,optipng,zopflipng,oxipnggifsiclesvgocwebp,dwebp,webpinfo,gif2webpheif-encavifenctiffcpmagickexiftoolcjxlicotooldnglabfor RAW to DNG conversion in--maxmode
Not every run needs every tool. Dependency checks are format-aware, so squeezit only asks for the binaries needed for the files you matched.
Self-Update
Squeezit can check for a new published version and update itself:
squeezit --check-update
squeezit -UInstaller detection works like this:
- On install,
squeezitrecords whether it was installed bynpmorbunin its config metadata - On update, it reuses that persisted installer when available
- If detection is ambiguous, pass
--pm npmor--pm bun
Examples:
squeezit -U --pm npm
squeezit -U --pm bunIf dependencies are missing, you can ask squeezit to install them:
squeezit --install-depsSupported installation targets:
- macOS via Homebrew
- Debian/Ubuntu via APT
Development
This project publishes a Node-targeted CLI, but uses Bun for local development.
Install dependencies:
bun installRun the source CLI:
bun run index.ts --helpBuild the published artifact:
bun run buildRun the compiled CLI locally:
node ./dist/index.js --helpValidate the project:
bun run typecheck
bun test