Інтеграція 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' })
// Валідувати формат через метаданні (не 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 upload — 1–2 робочі дні.







