Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Image Optimizer Laravel Package

spatie/image-optimizer

Optimize PNG, JPG, WEBP, AVIF, SVG and GIF images in PHP by running them through a chain of tools like JpegOptim, Optipng, Pngquant and SVGO. It auto-detects installed binaries and overwrites files in place with smaller versions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and, just as importantly, install the binaries — the PHP package is only a thin orchestrator around CLI tools, so a composer require alone does nothing.

composer require spatie/image-optimizer

For a Docker-based Laravel app, bake the binaries into the image (this is the step that breaks most first deployments):

RUN apt-get update && apt-get install -y --no-install-recommends \
    jpegoptim optipng pngquant gifsicle webp libavif-bin \
 && apt-get clean && rm -rf /var/lib/apt/lists/*
# svgo is not packaged; install via node or npm
RUN npm install -g svgo

Where to look first:

  1. OptimizerChainFactory::create() — the entry point; it detects which binaries exist on the machine and builds the chain. It is the only class most projects touch.
  2. src/Optimizers/* — one small class per binary (Jpegoptim, Optipng, Pngquant, Svgo, Gifsicle, Cwebp, Avifenc). Read one (Jpegoptim) and you understand them all: binaryName(), canHandle(Image), getCommand().
  3. Spatie\ImageOptimizer\Image — the tiny value object with path() and mime() used for dispatching.

First use case, run it right after an upload is stored:

use Spatie\ImageOptimizer\OptimizerChainFactory;

$path = $request->file('photo')->store('uploads', 'local');

OptimizerChainFactory::create()->optimize(storage_path('app/'.$path));

The file is overwritten in place with a smaller version. There is no return value — measure savings yourself with filesize() before and after.

Implementation Patterns

In-place vs. "write elsewhere". The default optimize($path) mutates the file. Passing a second argument keeps the original untouched and writes the result to a new path — useful when you need both the pristine upload (for reprocessing) and the optimized derivative:

$chain->optimize($originalPath, $optimizedPath);

Never optimize a remote path. The package shells out to local binaries; Storage::disk('s3')->url() or a stream wrapper will not work. Download to a temp file, optimize, push back:

$disk = Storage::disk('s3');
$tmp  = tempnam(sys_get_temp_dir(), 'img');

file_put_contents($tmp, $disk->get($key));

OptimizerChainFactory::create()
    ->setTimeout(30)
    ->optimize($tmp);

$disk->put($key, file_get_contents($tmp));
unlink($tmp);

Always do this in a queued job, not in the request. Optimization is CPU-bound and can take multiple seconds per image (cwebp alone runs 10 analysis passes). A ShouldQueue job keeps upload responses fast:

class OptimizeImageJob implements ShouldQueue
{
    public int $timeout = 120;

    public function __construct(public string $path) {}

    public function handle(): void
    {
        if (! is_file($this->path)) {
            return;
        }

        OptimizerChainFactory::create()
            ->setTimeout(20)
            ->throws(fn (Throwable $e) => report($e))
            ->optimize($this->path);
    }
}

Configure explicit failure handling. By default failures are swallowed, which means a missing binary silently ships unoptimized images. In staging/CI, fail loudly:

$chain->throws()->optimize($path);          // abort on first failure
$chain->throws(false)->optimize($path);     // explicit "log and continue"
$chain->throws(function (Throwable $e, Optimizer $optimizer, Image $image) {
    report($e); // return to continue, throw to abort
})->optimize($path);

Attach a logger when you don't trust the environment. The chain logs which optimizers ran, the exact commands, and their output — the single fastest way to diagnose "why is this PNG still 2 MB":

$chain->useLogger(Log::getLogger())->optimize($path);

Build a custom chain when defaults don't match your brand. For a photography site you may want JPEG quality 92 and no PNG quantization; for a marketplace thumbnail pipeline the aggressive defaults are perfect:

use Spatie\ImageOptimizer\OptimizerChain;
use Spatie\ImageOptimizer\Optimizers\{Jpegoptim, Optipng};

$chain = (new OptimizerChain)
    ->addOptimizer(new Jpegoptim(['-m92', '--strip-all', '--all-progressive']))
    ->addOptimizer(new Optipng(['-i0', '-o2']));

Bind one configured chain in a service provider and inject it, rather than calling the factory in ten places:

$this->app->singleton(OptimizerChain::class, fn () => OptimizerChainFactory::create()
    ->setTimeout(20)
    ->throws(...));

Optimizers without a binary. If you route images through an external optimization API (TinyPNG, Cloudflare, an internal service), extend BaseSelfHandlingOptimizer and implement canHandle() + handle(); the chain delegates instead of building a process command. Implement it once and it composes with the bundled optimizers in the same chain.

Custom binary optimizers. Implement Spatie\ImageOptimizer\Optimizers\Optimizer and return the full command from getCommand(). Reuse the pattern from the shipped classes to keep escaping consistent.

Testing. Don't assert on byte sizes (they vary by binary version and platform). Assert instead that (a) the file still exists and is a valid image, and (b) your job was dispatched. For unit tests, swap in a fake OptimizerChain — the chain has no final methods, so a no-op subclass or a mocked Optimizer is enough.

Gotchas and Tips

  • Silent no-ops are the #1 failure mode. If none of the binaries are installed, OptimizerChainFactory::create() returns a chain that does nothing and throws nothing. Add a deploy-time smoke test (which jpegoptim pngquant optipng) or a health check that runs the chain on a fixture and asserts the file got smaller.
  • exec/proc_open may be disabled on shared hosting and is not permitted by default in many serverless runtimes (Laravel Vapor). Test in the target environment, not locally.
  • Local and production can produce different output. The chain is assembled from whichever binaries happen to exist, so a Mac dev machine and a Debian container may apply different optimizers to the same JPEG. Log the executed commands once per environment and accept that regenerating assets later may change bytes.
  • Lossy by default. pngquant quantization is lossy, JPEG is recompressed at 85, WebP at q90, AVIF at cq-level 23. Do not run this on source assets you might need to re-export from — keep originals, optimize derivatives only.
  • EXIF and metadata are stripped (--strip-all). That's desirable for privacy but also destroys orientation/GPS/ICC data and any copyright fields. If you rely on EXIF rotation, orient the image before optimizing.
  • SVGO 1 flags vs. modern svgo. The package targets SVGO 1-era options (--disable=cleanupIDs, --disable=removeViewBox); npm install -g svgo today ships svgo 3, where those flags no longer exist and the command can fail or behave differently. Pin an svgo version in your image, or wrap SVGO in your own optimizer with a v3-compatible config. Also, SVGO can and does break hand-authored SVGs — spot-check complex artwork.
  • WEBP/AVIF optimizers re-encode, they don't just recompress. cwebp/avifenc decode and re-encode the file, which re-quantizes lossy content and can flatten animated WebP frames. Verify animated assets survive the trip, or exclude them via canHandle().
  • OptimizerChainFactory::create() is the correct class name. The README's "custom optimizer" example imports a non-existent ImageOptimizerFactory — a copy-paste trap.
  • Set a timeout explicitly. Left unbounded, a pathological or hung binary can stall a worker indefinitely. setTimeout(10) applies per optimizer in the chain, not to the whole run, so budget accordingly against your queue $timeout.
  • Watch out for in-place races. Overwriting a file that a concurrent request is reading (or a CDN is pulling) yields truncated responses. Optimize into a temp path, then atomically rename() into place, or write to a new key and update the DB pointer.
  • Re-running is not idempotent-cheap. Optimizing an already-optimized image is usually harmless but burns CPU and can degrade quality further (double JPEG compression). Gate it with a column like optimized_at or a job uniqueness lock.
  • File permissions matter. The PHP-FPM user needs write access to the file and execute access to the binaries. In practice this means chown-consistent storage directories and no sudo-installed binaries in /usr/local/sbin.
  • Extension point worth knowing: the whole chain is just a collection of Optimizer objects. Anything you dislike about the defaults — quality, strip behaviour, ordering — is overridable by constructing the chain yourself, without forking the package.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource