onlinedigital / laravel-perfect-image
Automatic responsive image optimization for Laravel
Package info
github.com/OnlineDigital/laravel-perfect-image
pkg:composer/onlinedigital/laravel-perfect-image
Requires
- php: ^8.1
- laravel/framework: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Automatic responsive image optimization for Laravel. Detects rendered image dimensions, caches the most common resolutions, and generates srcset attributes automatically.
Features
- Automatic detection of rendered image dimensions via JavaScript
- Caches the most common resolutions per image
- Driver-based URL generation (supports multiple image services)
- Blade directive for easy usage
- Lazy resize tracking
Installation
composer require onlinedigital/laravel-perfect-image
Add Middleware (Optional - for automatic JS injection)
Add to your bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'perfect-image' => \OnlineDigital\PerfectImage\Http\Middleware\InjectPerfectImageJs::class, ]); }) ->create();
Or apply to specific routes:
Route::get('/home', [HomeController::class, 'index'])->middleware('perfect-image');
Publish Config
php artisan vendor:publish --provider="OnlineDigital\\PerfectImage\\PerfectImageServiceProvider" --tag="perfect-image-config"
Config Options
// config/perfect-image.php return [ // Cache driver to store resolution data 'cache_driver' => env('PERFECT_IMAGE_CACHE_DRIVER', 'file'), // Enable the collection of rendered image dimensions (tracking attributes, // JS injection and the observer endpoint). When disabled, the endpoint // answers with HTTP 403 {"code": "collect_dimensions_disabled"}. 'collect_dimensions' => env('PERFECT_IMAGE_COLLECT_DIMENSIONS', false), // How many of the most common resolutions to keep 'max_resolutions' => 5, // Minimum width difference to count as a new resolution 'min_width_diff' => 50, // The route name for the observer endpoint 'route_name' => '_perfect_image_observer', // Default image driver (url, cloudinary, etc.) 'driver' => env('PERFECT_IMAGE_DRIVER', 'url'), // Driver-specific configuration 'drivers' => [ 'url' => [ // For simple URL manipulation (e.g., adding query params) 'base_url' => env('APP_URL'), 'param_format' => '{url}?w={width}&h={height}', // or {url}/{width}x{height} ], 'cloudinary' => [ 'cloud_name' => env('CLOUDINARY_CLOUD_NAME'), 'base_url' => 'https://res.cloudinary.com/{cloud_name}/image/upload', ], // Add more drivers as needed ], ];
Usage
Basic Usage
In your Blade templates:
<img @perfect_image_srcset('unique-key', $imagepath) />
Example:
<img @perfect_image_srcset('hero-image', asset('images/hero.jpg')) />
This generates:
<!-- If resolutions are cached --> <img src="https://example.com/images/hero.jpg?w=320" srcset="https://example.com/images/hero.jpg?w=320 320w, https://example.com/images/hero.jpg?w=640 640w, ..." sizes="(max-width: 800px) 100vw, 640px"> <!-- If no cached resolutions and collect_dimensions is enabled --> <img src="https://example.com/images/hero.jpg" data-perfect-image-id="hero-image" data-perfect-image-src="https://example.com/images/hero.jpg" onload="window.PerfectImage&&window.PerfectImage.observe(this)"> <!-- If no cached resolutions and collect_dimensions is disabled --> <img src="https://example.com/images/hero.jpg">
sizes is learned from how the rendered width changes with the viewport width
(100vw until something has been learned).
Custom Sizes
The third argument is a list of widths that overrides the learned resolutions:
<img {{ perfect_image_srcset('product-image', $product->image, ['640', '1024', '1280']) }}>
The directive accepts the same arguments:
<img @perfect_image_srcset('product-image', $product->image, ['640', '1024', '1280']) />
JavaScript
While collect_dimensions is enabled, the JavaScript is injected automatically
(before </body>) on pages that contain tracked images. It does not need jQuery.
Creating Custom Drivers
Create a class that implements OnlineDigital\PerfectImage\Contracts\Driver:
<?php namespace App\Drivers; use OnlineDigital\PerfectImage\Contracts\Driver; class MyCustomDriver implements Driver { public function generateUrl(string $url, int $width, ?int $height = null): string { // Generate the URL for the resized image return "https://my-cdn.com/{$width}x{$height}/{$url}"; } }
Register it in a service provider:
$this->app->bind(\OnlineDigital\PerfectImage\Contracts\Driver::class, MyCustomDriver::class);
How It Works
- First Visit: If collection is enabled and no resolutions are cached for an image, the Blade directive adds
src, data attributes and an onload handler - JavaScript Tracking: On page load and whenever an image or the window is resized (throttled, with a trailing call), JS measures every tracked image
- Dimension Collection: Once enough samples were gathered, all images are sent in a single batch to the
_perfect_image_observerendpoint - Caching: The server saves the most common resolutions (at most
max_resolutions) and asizesvalue - Applying: The response contains
src,srcsetandsizes, which JS applies without reloading the page - Subsequent Visits: srcset is generated from cached resolutions
Collecting all resolutions automatically (Playwright)
Instead of waiting for someone to resize a browser window, a script can open the pages, sweep the viewport from the smallest phone (320px) to a 2K desktop (2560px) and trigger the collection. This is also the workflow meant for coding agents: after adding a page or a component, run the collector for those URLs and commit the result.
Setup (once)
npm install -D playwright npx playwright install chromium
Set the JSON file store (PERFECT_IMAGE_CACHE_DRIVER=storage_file). Collected data is written to
resources/perfect-image.json, so it can be committed and deployed with the code.
Run
The application must be running (for example php artisan serve) on the same machine/filesystem.
# Specific pages (new page, new component): all of them are visited php artisan perfect-image:collect --urls=/,/about,/product/12 # Discover pages: robots.txt -> Sitemap:, fallback /sitemap.xml, then a few pages per template php artisan perfect-image:collect --sitemap # Pages built from the application routes (static routes + routes bound to Eloquent models) php artisan perfect-image:collect --from-routes # Discard the previously collected data of the visited images first php artisan perfect-image:collect --urls=/product/12 --fresh
What the command does:
- Turns the collection on with a temporary flag file (
storage/app/perfect_image_collect.flag). The flag is ignored in production, so there is no per request cost there. It is removed afterwards, and the previous state is restored even if the command is interrupted. - Runs
scripts/collect.mjswith Playwright. For every page it loads the page, forces lazy images to load, resizes the viewport step by step (and exactly at the usual breakpoints) and measures every tracked image. - Calls the collect API once per page and prints a JSON report (
--jsonfor JSON only).
Useful options:
| Option | Default | |
|---|---|---|
--base-url |
app.url |
Running application |
--max-per-template |
3 | Discovered pages visited per route (/product/{slug}) or URL directory |
--all |
Visit every discovered URL | |
--min-width, --max-width, --step |
320, 2560, 40 | Viewport sweep |
--concurrency |
2 | Pages in parallel (1 with --fresh) |
--header="Name: value" |
Extra request headers | |
--storage-state=file.json |
Playwright storage state for pages behind a login |
Notes:
- Only images rendered with
@perfect_image_srcset/perfect_image_srcset()are measured. - Images inside carousels, tabs, modals or hover menus have no size until they are visible. Visit the
page state in a custom Playwright script, or measure them with real users (
collect_dimensions). --from-routesvisits static GET routes. Do not use it if one of them has side effects.- Models are resolved for routes with exactly one parameter that is type-hinted with an Eloquent model.
Without artisan
The script can be used directly (collection must already be enabled, see collect_dimensions):
node vendor/onlinedigital/laravel-perfect-image/scripts/collect.mjs \
--base-url=http://localhost:8000 --urls=/,/about
Exit codes: 0 ok, 1 errors, 2 nothing measured or images left unmeasured, 3 collection disabled.
Coding agents
See AGENTS.md for the short workflow to give to an agent.
Route
The package automatically registers the observer route:
POST _perfect_image_observer
Request body (resolutions items are [width, height] or [width, height, viewportWidth]):
{
"id": "unique-image-key",
"src": "https://example.com/image.jpg",
"resolutions": [[320, 240], [640, 480], [1024, 768]]
}
Several images can be sent at once (this is what the JavaScript does):
{
"images": [
{"id": "hero-image", "src": "https://example.com/hero.jpg", "resolutions": [[320, 240, 360]]}
]
}
Responses: 200 with the resulting src/srcset/sizes per image id, 422 for an invalid
payload, 403 (collect_dimensions_disabled) when collect_dimensions is off.
License
MIT