Генерация миниатюр изображений: решаем проблему LCP и веса страниц
Сайт с большим количеством изображений тормозит? Каждая страница грузит оригиналы 4000x3000 пикселей — LCP зашкаливает, пользователи уходят. Наш опыт показывает, что автоматическая генерация миниатюр на сервере — единственный способ удержать Core Web Vitals в норме без ручной обработки каждого файла. Мы реализуем решение за 2–3 дня под ключ: настройка очередей, форматы WebP/AVIF, кэширование на CDN. Это снижает объем передаваемых данных на 40–60% и улучшает LCP на 30–50%, что напрямую влияет на конверсию.
Как работает генерация миниатюр в очереди?
Асинхронная генерация — стандарт для современных проектов с высокой нагрузкой на загрузку. После того как пользователь загрузил оригинал в S3 или локальное хранилище, в очередь (Laravel Job, BullMQ, или AWS SQS) отправляется задача. Она создаёт несколько размеров: thumb (200x200) для списков, medium (600x400) для карточек, large (1200x800) для галерей. Пользователь не ждёт — ответ приходит сразу, а миниатюры появляются через секунды. При этом очередь не блокирует веб-сервер: обработка происходит в фоновом процессе.
Почему стоит выбрать WebP/AVIF?
Современные форматы сжатия со потерями дают 25–50% экономии размера без заметного падения качества. Для браузеров, не поддерживающих AVIF, автоматически отдаётся WebP через элемент <picture>. Это снижает LCP на 30–50%. AVIF (на основе AV1) обеспечивает наилучшее сжатие, но требует больше ресурсов CPU для кодирования. WebP — универсальный выбор, поддерживаемый 96% браузеров.
Как выбрать стратегию генерации: очередь vs ленивая генерация?
Для сайтов с высокой частотой загрузок (фотогалереи, маркетплейсы) асинхронная очередь — единственный вариант: она не блокирует ответ и позволяет масштабировать обработку горизонтально. Ленивая генерация через Glide подходит для проектов с редкими загрузками или когда размеры миниатюр заранее неизвестны. Она генерирует изображение по первому запросу и кэширует его. Однако при первом обращении пользователь ждёт генерации. Мы помогаем выбрать стратегию на основе ваших метрик: количество загрузок в день, средний размер изображений, бюджет на инфраструктуру.
Что входит в нашу работу
- Аудит текущей системы хранения и обработки изображений
- Выбор стратегии: очередь или ленивая генерация
- Реализация генерации миниатюр с нужными размерами и форматами
- Настройка кэширования (CDN, Cache-Control)
- Интеграция с существующим API и хранилищем (S3, облачный сервер)
- Документация по использованию и поддержке
Почему Sharp справляется быстрее конкурентов?
Библиотека Sharp (на базе libvips) выполняет операции в 4–5 раз быстрее, чем ImageMagick или GD для типовых задач. Это достигается за счёт работы с изображением в оперативной памяти без промежуточных файлов и эффективного использования кэша процессора. Для Node.js-проектов Sharp — стандарт де-факто. Для PHP-проектов рекомендуем Intervention Image с очередями или Glide для ленивой генерации.
Форматы и оптимизация: сравнение
| Формат | Размер относительно JPEG | Совместимость | Рекомендуемое качество |
|---|---|---|---|
| JPEG | 100% (база) | Все браузеры | 85% |
| WebP | 70–80% | 96%+ | 80% |
| AVIF | 40–60% | 90%+ | 60% |
// Выбор формата по поддержке браузером
const output = sharp(buffer)
.resize(800)
.toFormat(supportsAvif ? 'avif' : supportsWebp ? 'webp' : 'jpeg', {
quality: supportsAvif ? 60 : supportsWebp ? 80 : 85,
});
AVIF даёт до 50% экономии относительно JPEG при том же качестве. WebP поддерживается всеми современными браузерами. Для максимальной совместимости используйте <picture> с несколькими форматами.
Laravel: Intervention Image + очередь
// Модель с автоматической генерацией миниатюр
class Image extends Model
{
const SIZES = [
'thumb' => [200, 200],
'medium' => [600, 400],
'large' => [1200, 800],
];
}
// Job для асинхронной генерации
class GenerateImageThumbnails implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function __construct(private Image $image) {}
public function handle(): void
{
$originalPath = Storage::disk('s3')->path($this->image->path);
$img = \Intervention\Image\Facades\Image::make($originalPath);
foreach (Image::SIZES as $size => [$width, $height]) {
$resized = clone $img;
$resized->fit($width, $height); // crop по центру
$thumbPath = str_replace('original/', "{$size}/", $this->image->path);
Storage::disk('s3')->put($thumbPath, $resized->encode('webp', 85)->__toString());
}
$this->image->update(['processed' => true]);
}
}
// Контроллер загрузки
public function store(Request $request): JsonResponse
{
$path = Storage::disk('s3')->putFile('original', $request->file('image'));
$image = Image::create([
'path' => $path,
'user_id' => auth()->id(),
'processed' => false,
]);
GenerateImageThumbnails::dispatch($image);
return response()->json(['id' => $image->id]);
}
Node.js: Sharp
Sharp — самая быстрая Node.js-библиотека для обработки изображений (на базе libvips).
import sharp from 'sharp';
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
const SIZES = {
thumb: { width: 200, height: 200 },
medium: { width: 600, height: 400 },
large: { width: 1200, height: 800 },
} as const;
async function generateThumbnails(s3Key: string): Promise<Record<string, string>> {
const s3 = new S3Client({ region: 'eu-west-1' });
// Скачать оригинал
const { Body } = await s3.send(new GetObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: s3Key,
}));
const buffer = Buffer.from(await (Body as any).transformToByteArray());
const results: Record<string, string> = {};
await Promise.all(
Object.entries(SIZES).map(async ([name, { width, height }]) => {
const thumbnail = await sharp(buffer)
.resize(width, height, { fit: 'cover', position: 'centre' })
.webp({ quality: 85 })
.toBuffer();
const thumbKey = s3Key.replace('original/', `${name}/`).replace(/\.[^.]+$/, '.webp');
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: thumbKey,
Body: thumbnail,
ContentType: 'image/webp',
CacheControl: 'public, max-age=31536000',
}));
results[name] = thumbKey;
})
);
return results;
}
Ленивая генерация через Glide (PHP)
Glide генерирует миниатюры по запросу с подписанным URL:
// Маршрут для изображений
Route::get('/img/{path}', function (Request $request, string $path) {
$server = League\Glide\ServerFactory::create([
'source' => Storage::disk('s3')->getDriver(),
'cache' => Storage::disk('local')->getDriver(),
'cache_path_prefix' => '.cache',
'base_url' => '/img',
'max_image_size' => 2000 * 2000,
]);
// Проверка подписи URL
League\Glide\Signatures\SignatureFactory::create(config('app.key'))
->validateRequest('/img/' . $path, $request->all());
return $server->getImageResponse($path, $request->all());
})->where('path', '.*');
// Генерация подписанного URL
$url = (new League\Glide\Urls\UrlBuilderFactory)
->create('/img', config('app.key'))
->getUrl('uploads/photo.jpg', ['w' => 400, 'h' => 300, 'fit' => 'crop']);
Срок реализации и стоимость
Генерация миниатюр в очереди (Laravel Job или BullMQ Worker) с сохранением в S3: 2–3 дня. С lazy generation через Glide и CDN-кэшированием: 3–4 дня. Сложные проекты с интеграцией нескольких CMS — до 5 дней. Стоимость рассчитывается индивидуально в зависимости от сложности и стека. Свяжитесь с нами, чтобы получить консультацию и точные сроки. Закажите аудит и оптимизацию изображений — мы подберем оптимальную стратегию для вашего проекта.
Наши инженеры имеют 5+ лет опыта в оптимизации высоконагруженных сайтов. Уже более 30 проектов с подобной архитектурой успешно работают в продакшене. Получите консультацию — мы оценим ваш проект и предложим решение под ключ, гарантируя снижение LCP на 30–50% после внедрения.







