Sharp Integration (Node.js) for Server-Side Image Processing
Sharp — Node.js library based on libvips, an order of magnitude faster than Jimp or Canvas API. Processes JPEG, PNG, WebP, AVIF, TIFF, GIF, SVG without quality loss, doesn't require ImageMagick and works 4–5 times faster with half the memory consumption compared to alternatives.
Installation and Basic Setup
npm install sharp
# Sharp ships with precompiled libvips binaries
# for Linux x64, macOS arm64, Windows x64
Sharp uses streaming processing — images aren't loaded fully into memory:
const sharp = require('sharp')
// Basic pipeline: resize + WebP + save
await sharp('./input/photo.jpg')
.resize(800, 600, {
fit: 'inside', // fit within frame without cropping
withoutEnlargement: true // don't enlarge small images
})
.webp({ quality: 82, effort: 4 })
.toFile('./output/photo.webp')
Output Formats and Quality Parameters
| Format | Method | Recommended Quality |
|---|---|---|
| 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 |
// Generate multiple formats from single source
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 for modern browsers
resized.clone()
.webp({ quality: 82, effort: 4 })
.toFile(`${outputDir}/${baseName}.webp`),
// AVIF for Chrome/Firefox
resized.clone()
.avif({ quality: 55, effort: 4 })
.toFile(`${outputDir}/${baseName}.avif`),
// JPEG as fallback
resized.clone()
.jpeg({ quality: 85, mozjpeg: true })
.toFile(`${outputDir}/${baseName}.jpg`),
])
return { width: meta.width, height: meta.height }
}
Processing EXIF and Orientation
Sharp automatically reads EXIF orientation but doesn't apply it by default:
const image = sharp(buffer)
.rotate() // auto-rotate by EXIF orientation
.withMetadata({ // preserve metadata (except GPS if needed for privacy)
exif: {
IFD0: { Copyright: 'My Company 2024' }
}
})
To remove GPS data when publishing publicly:
// Remove all metadata (EXIF, IPTC, XMP)
.withMetadata(false)
// Or keep only ICC profile for correct colors
.withMetadata({ icc: true })
Integration with Multer (Express)
const multer = require('multer')
const { v4: uuidv4 } = require('uuid')
// Store in memory, process with Sharp before saving to disk/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' })
// Validate format via metadata (not MIME header — it can be spoofed)
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
}
Watermark and Compositing
async function addWatermark(imageBuffer, watermarkPath) {
const image = sharp(imageBuffer)
const { width, height } = await image.metadata()
// Scale watermark to 20% of image width
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()
}
Performance: Concurrency and Threads
Sharp uses all CPUs by default. In production, limit this:
sharp.concurrency(2) // maximum 2 libvips threads
// For high load — queue via p-limit
const pLimit = require('p-limit')
const limit = pLimit(4) // 4 parallel tasks
const tasks = images.map(img =>
limit(() => processImage(img))
)
const results = await Promise.all(tasks)
Error Handling
async function safeProcess(buffer) {
try {
const meta = await sharp(buffer).metadata()
// Protection from 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
}
}
Timeline
Sharp integration with Multer, multiple output formats, and S3 upload — 1–2 working days.







