Серверная оптимизация изображений
Серверная оптимизация конвертирует загруженные изображения в современные форматы (WebP, AVIF), подбирает оптимальное качество и размер, снижает объём передаваемых данных на 40–70%.
Node.js: Sharp pipeline
import sharp from 'sharp';
interface OptimizeOptions {
maxWidth?: number;
quality?: number;
format?: 'webp' | 'avif' | 'jpeg';
}
async function optimizeImage(inputBuffer: Buffer, options: OptimizeOptions = {}): Promise<Buffer> {
const { maxWidth = 1920, quality = 80, format = 'webp' } = options;
return sharp(inputBuffer)
.resize(maxWidth, undefined, {
withoutEnlargement: true,
fit: 'inside',
})
.toFormat(format, {
quality,
effort: 4, // баланс скорость/размер
})
.withMetadata({ orientation: undefined }) // убрать EXIF rotation
.toBuffer();
}
// Middleware для lazy оптимизации
app.get('/images/:key', async (req, res) => {
const { key } = req.params;
const { w, q = '80', f = 'webp' } = req.query;
// Проверить кэш
const cacheKey = `${key}:${w}:${q}:${f}`;
const cached = await s3.getObject({ Key: `optimized/${cacheKey}.${f}` }).catch(() => null);
if (cached) {
return res.type(`image/${f}`).send(await streamToBuffer(cached.Body));
}
// Получить оригинал
const original = await s3.getObject({ Key: `originals/${key}` });
const buffer = await streamToBuffer(original.Body);
// Оптимизировать
const optimized = await optimizeImage(buffer, {
maxWidth: w ? parseInt(w as string) : 1920,
quality: parseInt(q as string),
format: f as 'webp' | 'avif' | 'jpeg',
});
// Сохранить в кэш
await s3.putObject({
Key: `optimized/${cacheKey}.${f}`,
Body: optimized,
ContentType: `image/${f}`,
CacheControl: 'public, max-age=31536000',
});
res.type(`image/${f}`).send(optimized);
});
PHP: оптимизация при загрузке
use Intervention\Image\Facades\Image;
class ImageOptimizationService
{
const FORMATS = ['webp', 'avif'];
const SIZES = [400, 800, 1200, 1920];
public function process(UploadedFile $file): array
{
$image = Image::make($file->getPathname());
// Убрать EXIF и повернуть по ориентации
$image->orientate()->stripExif();
$paths = [];
foreach (self::SIZES as $width) {
if ($image->width() < $width) continue;
$resized = clone $image;
$resized->resize($width, null, fn($c) => $c->aspectRatio()->upsize(false));
foreach (self::FORMATS as $format) {
$quality = $format === 'avif' ? 60 : 82;
$key = "images/{$width}w/{$this->generateKey()}.{$format}";
Storage::disk('s3')->put(
$key,
$resized->encode($format, $quality)->__toString(),
['CacheControl' => 'public, max-age=31536000']
);
$paths[$format][$width] = $key;
}
}
return $paths;
}
}
HTML: responsive images с srcset
// Blade хелпер для отображения оптимизированных изображений
function responsive_img(array $paths, string $alt, string $sizes = '100vw'): string
{
$avifSrcset = collect($paths['avif'] ?? [])->map(fn($path, $width) =>
Storage::disk('s3')->url($path) . " {$width}w"
)->join(', ');
$webpSrcset = collect($paths['webp'] ?? [])->map(fn($path, $width) =>
Storage::disk('s3')->url($path) . " {$width}w"
)->join(', ');
$fallback = Storage::disk('s3')->url(end($paths['webp']));
return <<<HTML
<picture>
<source type="image/avif" srcset="{$avifSrcset}" sizes="{$sizes}">
<source type="image/webp" srcset="{$webpSrcset}" sizes="{$sizes}">
<img src="{$fallback}" alt="{$alt}" loading="lazy" decoding="async">
</picture>
HTML;
}
Nginx: конвертация WebP на лету
# Отдавать WebP если браузер поддерживает
map $http_accept $webp_suffix {
"~*webp" ".webp";
default "";
}
server {
location ~* \.(jpg|jpeg|png)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Срок реализации
Sharp pipeline при загрузке с генерацией WebP/AVIF в нескольких размерах: 2–3 дня. С Nginx WebP конвертацией на лету и CDN: 3–4 дня.







