Интеграция Sharp (Node.js) для серверной обработки изображений
Sharp — Node.js-библиотека на базе libvips, на порядок быстрее Jimp или Canvas API. Обрабатывает JPEG, PNG, WebP, AVIF, TIFF, GIF, SVG без потери качества, не требует ImageMagick и работает в 4–5 раз быстрее аналогов при вдвое меньшем потреблении памяти.
Установка и базовая настройка
npm install sharp
# Sharp поставляется с предкомпилированными бинарниками libvips
# для Linux x64, macOS arm64, Windows x64
Sharp использует потоковую обработку — изображение не загружается полностью в память:
const sharp = require('sharp')
// Базовый pipeline: resize + WebP + сохранение
await sharp('./input/photo.jpg')
.resize(800, 600, {
fit: 'inside', // вписать в рамку без обрезки
withoutEnlargement: true // не увеличивать маленькие изображения
})
.webp({ quality: 82, effort: 4 })
.toFile('./output/photo.webp')
Форматы вывода и параметры качества
| Формат | Метод | Рекомендуемое качество |
|---|---|---|
| JPEG | .jpeg({ quality, mozjpeg }) |
80–85, mozjpeg: true |
| WebP | .webp({ quality, effort }) |
80–85, effort: 4 |
| AVIF | .avif({ quality, effort }) |
50–60, effort: 4 |
| PNG | .png({ compressionLevel }) |
compressionLevel: 6–8 |
// Генерация нескольких форматов из одного источника
async function convertToModernFormats(inputPath, outputDir, baseName) {
const image = sharp(inputPath)
const meta = await image.metadata()
const resized = image.resize(1200, null, {
fit: 'inside',
withoutEnlargement: true
})
await Promise.all([
// WebP для современных браузеров
resized.clone()
.webp({ quality: 82, effort: 4 })
.toFile(`${outputDir}/${baseName}.webp`),
// AVIF для Chrome/Firefox
resized.clone()
.avif({ quality: 55, effort: 4 })
.toFile(`${outputDir}/${baseName}.avif`),
// JPEG как fallback
resized.clone()
.jpeg({ quality: 85, mozjpeg: true })
.toFile(`${outputDir}/${baseName}.jpg`),
])
return { width: meta.width, height: meta.height }
}
Обработка EXIF и ориентации
Sharp автоматически читает EXIF-ориентацию, но не применяет её по умолчанию:
const image = sharp(buffer)
.rotate() // auto-rotate по EXIF orientation
.withMetadata({ // сохранить метаданные (кроме GPS если нужна приватность)
exif: {
IFD0: { Copyright: 'My Company 2024' }
}
})
Для удаления GPS-данных при публичной публикации:
// Удалить все метаданные (EXIF, IPTC, XMP)
.withMetadata(false)
// Или оставить только ICC-профиль для корректных цветов
.withMetadata({ icc: true })
Интеграция с Multer (Express)
const multer = require('multer')
const { v4: uuidv4 } = require('uuid')
// Хранить в памяти, обрабатывать Sharp'ом до сохранения на диск/S3
const upload = multer({ storage: multer.memoryStorage() })
app.post('/api/upload', upload.single('image'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file' })
// Валидировать формат через metadata (не MIME-заголовок — его можно подделать)
let meta
try {
meta = await sharp(req.file.buffer).metadata()
} catch {
return res.status(422).json({ error: 'Invalid image' })
}
const allowedFormats = ['jpeg', 'png', 'webp', 'gif', 'avif']
if (!allowedFormats.includes(meta.format)) {
return res.status(422).json({ error: `Format ${meta.format} not allowed` })
}
const id = uuidv4()
const variants = await processAndUpload(req.file.buffer, id)
res.json({ id, variants })
})
async function processAndUpload(buffer, id) {
const image = sharp(buffer).rotate() // EXIF auto-rotate
const sizes = {
thumb: { width: 150, height: 150, fit: 'cover' },
medium: { width: 800 },
large: { width: 1920 }
}
const results = {}
for (const [name, dims] of Object.entries(sizes)) {
const processed = await image.clone()
.resize(dims.width, dims.height || null, {
fit: dims.fit || 'inside',
withoutEnlargement: true
})
.webp({ quality: 82, effort: 4 })
.toBuffer()
const key = `images/${id}/${name}.webp`
await s3.putObject({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: processed,
ContentType: 'image/webp',
CacheControl: 'public, max-age=31536000, immutable'
}).promise()
results[name] = key
}
return results
}
Водяной знак и наложение
async function addWatermark(imageBuffer, watermarkPath) {
const image = sharp(imageBuffer)
const { width, height } = await image.metadata()
// Масштабировать watermark под 20% ширины изображения
const wmSize = Math.floor(width * 0.2)
const watermark = await sharp(watermarkPath)
.resize(wmSize)
.toBuffer()
return image
.composite([{
input: watermark,
gravity: 'southeast',
blend: 'over'
}])
.toBuffer()
}
Производительность: concurrency и потоки
Sharp по умолчанию использует все CPU. В production ограничивают:
sharp.concurrency(2) // максимум 2 потока libvips
// Для высокой нагрузки — очередь через p-limit
const pLimit = require('p-limit')
const limit = pLimit(4) // 4 параллельных задачи
const tasks = images.map(img =>
limit(() => processImage(img))
)
const results = await Promise.all(tasks)
Обработка ошибок
async function safeProcess(buffer) {
try {
const meta = await sharp(buffer).metadata()
// Защита от огромных изображений (decompression bomb)
if (meta.width * meta.height > 50_000_000) {
throw new Error('Image too large: exceeds 50MP limit')
}
return await sharp(buffer)
.resize(2000, 2000, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer()
} catch (err) {
if (err.message.includes('Input buffer contains unsupported image format')) {
throw new TypeError('Unsupported image format')
}
throw err
}
}
Срок выполнения
Интеграция Sharp с Multer, несколькими форматами вывода и загрузкой в S3 — 1–2 рабочих дня.







