Đăng nhập

Nén ảnh WebP trong Laravel với Intervention Image v3

Hướng dẫn đầy đủ nén ảnh WebP trong Laravel bằng Intervention Image v3 (Imagick): cài đặt, Service pattern, Controller + Request, lưu Storage, tối ưu chất lượng, lazy‑load và SEO.
Nén ảnh WebP trong Laravel với Intervention Image v3

Cài đặt và cấu hình

1) Cài package Intervention Image v3

composer require intervention/image

2) Chọn driver Imagick (khuyến nghị)

Cài Imagick extension cho PHP (tùy môi trường):

# Ubuntu
sudo apt-get install -y php-imagick
# Kiểm tra
php -m | grep imagick

Cấu hình driver trong code (v3 có API mới, không cần publish config):

use Intervention\\Image\\ImageManager;
use Intervention\\Image\\Drivers\\Imagick\\Driver;

$manager = new ImageManager(new Driver());

Tài liệu: Intervention Image – Docs.


ImageCompressionService (Service Pattern)

Service sau gói gọn các trường hợp: từ path, URL, base64, trả về base64, và resize + nén.

<?php

namespace App\\Services;

use Intervention\\Image\\ImageManager;
use Intervention\\Image\\Drivers\\Imagick\\Driver;
use Illuminate\\Support\\Facades\\Log;

class ImageCompressionService
{
    protected ImageManager $manager;

    public function __construct()
    {
        $this->manager = new ImageManager(new Driver());
    }

    /**
     * Nén ảnh từ đường dẫn file về WebP
     */
    public function compressFromPath(string $inputPath, string $outputPath, int $quality = 80): bool
    {
        try {
            $image = $this->manager->read($inputPath);
            // Tối ưu: strip metadata
            $image->orientate();
            $image->toWebp($quality)->save($outputPath);
            return true;
        } catch (\\Throwable $e) {
            Log::error('Image compression failed: '.$e->getMessage());
            return false;
        }
    }

    /**
     * Nén ảnh từ URL về WebP
     */
    public function compressFromUrl(string $url, string $outputPath, int $quality = 80): bool
    {
        try {
            $image = $this->manager->read($url);
            $image->orientate();
            $image->toWebp($quality)->save($outputPath);
            return true;
        } catch (\\Throwable $e) {
            Log::error('Image compression from URL failed: '.$e->getMessage());
            return false;
        }
    }

    /**
     * Nén ảnh từ base64 về WebP
     */
    public function compressFromBase64(string $base64String, string $outputPath, int $quality = 80): bool
    {
        try {
            $image = $this->manager->read($base64String);
            $image->orientate();
            $image->toWebp($quality)->save($outputPath);
            return true;
        } catch (\\Throwable $e) {
            Log::error('Image compression from base64 failed: '.$e->getMessage());
            return false;
        }
    }

    /**
     * Nén ảnh và trả về data URI (base64)
     */
    public function compressToBase64(string $inputPath, int $quality = 80): ?string
    {
        try {
            $image = $this->manager->read($inputPath);
            $image->orientate();
            return $image->toWebp($quality)->toDataUri();
        } catch (\\Throwable $e) {
            Log::error('Image compression to base64 failed: '.$e->getMessage());
            return null;
        }
    }

    /**
     * Resize theo max width/height rồi nén WebP
     */
    public function resizeAndCompress(
        string $inputPath,
        string $outputPath,
        int $maxWidth,
        int $maxHeight,
        int $quality = 80
    ): bool {
        try {
            $image = $this->manager->read($inputPath);
            // scaleDown giữ tỉ lệ, không vượt quá khung
            $image->scaleDown($maxWidth, $maxHeight)
                  ->orientate()
                  ->toWebp($quality)
                  ->save($outputPath);
            return true;
        } catch (\\Throwable $e) {
            Log::error('Image resize and compression failed: '.$e->getMessage());
            return false;
        }
    }
}

Gist tham khảo: Service Compress Img Laravel

Tài liệu: Intervention Image – Docs


Lưu file đúng chuẩn (Storage)

  • Lưu vào storage/app/public/images/... để tiện public.

  • Tạo symlink nếu chưa có:

php artisan storage:link
  • Trả URL public: asset('storage/images/xyz.webp').


Ví dụ Controller + Request

// app/Http/Requests/StoreImageRequest.php
namespace App\\Http\\Requests;
use Illuminate\\Foundation\\Http\\FormRequest;

class StoreImageRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'image' => ['required','image','mimes:jpeg,png,jpg,webp','max:5120'], // 5MB
            'max_width' => ['nullable','integer','min:16','max:7680'],
            'max_height' => ['nullable','integer','min:16','max:4320'],
            'quality' => ['nullable','integer','min:1','max:100'],
        ];
    }
}
// app/Http/Controllers/ImageController.php
namespace App\\Http\\Controllers;

use App\\Http\\Requests\\StoreImageRequest;
use App\\Services\\ImageCompressionService;
use Illuminate\\Support\\Str;

class ImageController extends Controller
{
    public function store(StoreImageRequest $request, ImageCompressionService $svc)
    {
        $file = $request->file('image');
        $name = Str::uuid().'.webp';
        $path = storage_path('app/public/images/'.$name);

        $maxW = (int)($request->input('max_width', 1920));
        $maxH = (int)($request->input('max_height', 1080));
        $quality = (int)($request->input('quality', 80));

        $file->move(storage_path('app/tmp'), $tmp = storage_path('app/tmp/'.Str::uuid().'.tmp'));

        $ok = $svc->resizeAndCompress($tmp, $path, $maxW, $maxH, $quality);

        @unlink($tmp);

        abort_unless($ok, 500, 'Compress failed');

        return response()->json([
            'url' => asset('storage/images/'.$name),
            'width' => $maxW,
            'height' => $maxH,
            'quality' => $quality,
        ]);
    }
}

Route:

Route::post('/images', [ImageController::class, 'store'])->name('[images.store](<http://images.store>)');

Tối ưu thêm cho sản xuất

  • Resize trước rồi mới nén để giảm kích thước đáng kể.

  • Chọn chất lượng 70–85 cho WebP là hợp lý; ảnh hero có thể 85–90.

  • Strip metadata (EXIF) để giảm dung lượng, đã minh họa bằng orientate() và xuất lại.

  • Tạo nhiều kích thước (responsive images) và dùng srcset/sizes.

  • Lazy‑load: thêm loading="lazy" cho ảnh dưới màn hình đầu tiên.

  • Hàng loạt ảnh: đẩy xử lý vào queue để không chặn request.

  • Bảo mật: chỉ cho phép mimes ảnh hợp lệ, kiểm tra kích thước, từ chối file lạ.


Hiển thị ảnh (Blade)

<picture>
  <source srcset=" asset('storage/images/post-1920.webp') " type="image/webp">
  <img
    src=" asset('storage/images/post-1920.webp') "
    alt="Ảnh minh họa nén WebP trong Laravel"
    width="1920" height="1080"
    loading="lazy" decoding="async">
</picture>

FAQ

WebP có luôn nhỏ hơn JPEG/PNG không?

Đa phần là có, nhất là với ảnh chụp. Với đồ họa phẳng, SVG/PNG tối ưu vẫn có thể nhỏ hơn.

Không có Imagick thì sao?

Có thể dùng driver GD, nhưng chất lượng WebP và hiệu năng thường kém hơn Imagick.

Có cần giữ bản gốc?

Nên. Lưu bản gốc (original) để có thể re‑encode khi đổi chất lượng/kích thước.

Làm sao tránh ghi đè tên?

Đặt tên file bằng uuid() hoặc hash nội dung.


Tài liệu tham khảo

Bài trước

Thêm cuộn mượt cho web Laravel bằng Lenis (Vite)

Để lại bình luận của bạn

Email của bạn sẽ không được công khai. Các trường bắt buộc được đánh dấu *

Đăng ký nhận bản tin

Đăng ký bản tin email để nhận những bài viết mới nhất trực tiếp trong hộp thư của bạn.
Cảm hứng mỗi ngày, nói không với spam ✨